agileflow 2.85.0 → 2.87.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 +53 -50
- package/scripts/agileflow-welcome.js +25 -8
- 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 +206 -85
- package/scripts/session-boundary.js +3 -3
- package/scripts/session-manager.js +339 -18
- 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 +273 -689
- 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 +30 -22
- 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
|
@@ -117,19 +117,39 @@ function isSessionActive(sessionId) {
|
|
|
117
117
|
return isPidAlive(parseInt(lock.pid, 10));
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
// Clean up stale locks
|
|
121
|
-
function cleanupStaleLocks(registry) {
|
|
120
|
+
// Clean up stale locks (with detailed tracking)
|
|
121
|
+
function cleanupStaleLocks(registry, options = {}) {
|
|
122
|
+
const { verbose = false, dryRun = false } = options;
|
|
122
123
|
let cleaned = 0;
|
|
124
|
+
const cleanedSessions = [];
|
|
123
125
|
|
|
124
126
|
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
125
127
|
const lock = readLock(id);
|
|
126
|
-
if (lock
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
if (lock) {
|
|
129
|
+
const pid = parseInt(lock.pid, 10);
|
|
130
|
+
const isAlive = isPidAlive(pid);
|
|
131
|
+
|
|
132
|
+
if (!isAlive) {
|
|
133
|
+
// Track what we're cleaning and why
|
|
134
|
+
cleanedSessions.push({
|
|
135
|
+
id,
|
|
136
|
+
nickname: session.nickname,
|
|
137
|
+
branch: session.branch,
|
|
138
|
+
pid,
|
|
139
|
+
reason: 'pid_dead',
|
|
140
|
+
path: session.path,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (!dryRun) {
|
|
144
|
+
removeLock(id);
|
|
145
|
+
}
|
|
146
|
+
cleaned++;
|
|
147
|
+
}
|
|
129
148
|
}
|
|
130
149
|
}
|
|
131
150
|
|
|
132
|
-
|
|
151
|
+
// Return detailed info for display
|
|
152
|
+
return { count: cleaned, sessions: cleanedSessions };
|
|
133
153
|
}
|
|
134
154
|
|
|
135
155
|
// Get current git branch
|
|
@@ -156,8 +176,21 @@ function getCurrentStory() {
|
|
|
156
176
|
return null;
|
|
157
177
|
}
|
|
158
178
|
|
|
179
|
+
// Thread type enum values
|
|
180
|
+
const THREAD_TYPES = ['base', 'parallel', 'chained', 'fusion', 'big', 'long'];
|
|
181
|
+
|
|
182
|
+
// Auto-detect thread type from context
|
|
183
|
+
function detectThreadType(session, isWorktree = false) {
|
|
184
|
+
// Worktree sessions are parallel threads
|
|
185
|
+
if (isWorktree || (session && !session.is_main)) {
|
|
186
|
+
return 'parallel';
|
|
187
|
+
}
|
|
188
|
+
// Default to base
|
|
189
|
+
return 'base';
|
|
190
|
+
}
|
|
191
|
+
|
|
159
192
|
// Register current session (called on startup)
|
|
160
|
-
function registerSession(nickname = null) {
|
|
193
|
+
function registerSession(nickname = null, threadType = null) {
|
|
161
194
|
const registry = loadRegistry();
|
|
162
195
|
const cwd = process.cwd();
|
|
163
196
|
const branch = getCurrentBranch();
|
|
@@ -179,6 +212,10 @@ function registerSession(nickname = null) {
|
|
|
179
212
|
registry.sessions[existingId].story = story ? story.id : null;
|
|
180
213
|
registry.sessions[existingId].last_active = new Date().toISOString();
|
|
181
214
|
if (nickname) registry.sessions[existingId].nickname = nickname;
|
|
215
|
+
// Update thread_type if explicitly provided
|
|
216
|
+
if (threadType && THREAD_TYPES.includes(threadType)) {
|
|
217
|
+
registry.sessions[existingId].thread_type = threadType;
|
|
218
|
+
}
|
|
182
219
|
|
|
183
220
|
writeLock(existingId, pid);
|
|
184
221
|
saveRegistry(registry);
|
|
@@ -190,6 +227,11 @@ function registerSession(nickname = null) {
|
|
|
190
227
|
const sessionId = String(registry.next_id);
|
|
191
228
|
registry.next_id++;
|
|
192
229
|
|
|
230
|
+
const isMain = cwd === ROOT;
|
|
231
|
+
const detectedType = threadType && THREAD_TYPES.includes(threadType)
|
|
232
|
+
? threadType
|
|
233
|
+
: detectThreadType(null, !isMain);
|
|
234
|
+
|
|
193
235
|
registry.sessions[sessionId] = {
|
|
194
236
|
path: cwd,
|
|
195
237
|
branch,
|
|
@@ -197,13 +239,14 @@ function registerSession(nickname = null) {
|
|
|
197
239
|
nickname: nickname || null,
|
|
198
240
|
created: new Date().toISOString(),
|
|
199
241
|
last_active: new Date().toISOString(),
|
|
200
|
-
is_main:
|
|
242
|
+
is_main: isMain,
|
|
243
|
+
thread_type: detectedType,
|
|
201
244
|
};
|
|
202
245
|
|
|
203
246
|
writeLock(sessionId, pid);
|
|
204
247
|
saveRegistry(registry);
|
|
205
248
|
|
|
206
|
-
return { id: sessionId, isNew: true };
|
|
249
|
+
return { id: sessionId, isNew: true, thread_type: detectedType };
|
|
207
250
|
}
|
|
208
251
|
|
|
209
252
|
// Unregister session (called on exit)
|
|
@@ -291,7 +334,7 @@ function createSession(options = {}) {
|
|
|
291
334
|
};
|
|
292
335
|
}
|
|
293
336
|
|
|
294
|
-
// Register session
|
|
337
|
+
// Register session - worktree sessions are always parallel threads
|
|
295
338
|
registry.next_id++;
|
|
296
339
|
registry.sessions[sessionId] = {
|
|
297
340
|
path: worktreePath,
|
|
@@ -301,6 +344,7 @@ function createSession(options = {}) {
|
|
|
301
344
|
created: new Date().toISOString(),
|
|
302
345
|
last_active: new Date().toISOString(),
|
|
303
346
|
is_main: false,
|
|
347
|
+
thread_type: options.thread_type || 'parallel', // Worktrees default to parallel
|
|
304
348
|
};
|
|
305
349
|
|
|
306
350
|
saveRegistry(registry);
|
|
@@ -310,6 +354,7 @@ function createSession(options = {}) {
|
|
|
310
354
|
sessionId,
|
|
311
355
|
path: worktreePath,
|
|
312
356
|
branch: branchName,
|
|
357
|
+
thread_type: registry.sessions[sessionId].thread_type,
|
|
313
358
|
command: `cd "${worktreePath}" && claude`,
|
|
314
359
|
};
|
|
315
360
|
}
|
|
@@ -317,7 +362,7 @@ function createSession(options = {}) {
|
|
|
317
362
|
// Get all sessions with status
|
|
318
363
|
function getSessions() {
|
|
319
364
|
const registry = loadRegistry();
|
|
320
|
-
const
|
|
365
|
+
const cleanupResult = cleanupStaleLocks(registry);
|
|
321
366
|
|
|
322
367
|
const sessions = [];
|
|
323
368
|
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
@@ -332,7 +377,12 @@ function getSessions() {
|
|
|
332
377
|
// Sort by ID (numeric)
|
|
333
378
|
sessions.sort((a, b) => parseInt(a.id) - parseInt(b.id));
|
|
334
379
|
|
|
335
|
-
|
|
380
|
+
// Return count for backward compat, plus detailed info
|
|
381
|
+
return {
|
|
382
|
+
sessions,
|
|
383
|
+
cleaned: cleanupResult.count,
|
|
384
|
+
cleanedSessions: cleanupResult.sessions,
|
|
385
|
+
};
|
|
336
386
|
}
|
|
337
387
|
|
|
338
388
|
// Get count of active sessions (excluding current)
|
|
@@ -671,6 +721,172 @@ function integrateSession(sessionId, options = {}) {
|
|
|
671
721
|
return result;
|
|
672
722
|
}
|
|
673
723
|
|
|
724
|
+
// Session phases for Kanban-style visualization
|
|
725
|
+
const SESSION_PHASES = {
|
|
726
|
+
TODO: 'todo',
|
|
727
|
+
CODING: 'coding',
|
|
728
|
+
REVIEW: 'review',
|
|
729
|
+
MERGED: 'merged',
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
// Detect session phase based on git state
|
|
733
|
+
function getSessionPhase(session) {
|
|
734
|
+
// If merged_at field exists, session was merged
|
|
735
|
+
if (session.merged_at) {
|
|
736
|
+
return SESSION_PHASES.MERGED;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// If is_main, it's the merged/main column
|
|
740
|
+
if (session.is_main) {
|
|
741
|
+
return SESSION_PHASES.MERGED;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Check git state for the session
|
|
745
|
+
try {
|
|
746
|
+
const sessionPath = session.path;
|
|
747
|
+
if (!fs.existsSync(sessionPath)) {
|
|
748
|
+
return SESSION_PHASES.TODO;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Count commits since branch diverged from main
|
|
752
|
+
const mainBranch = getMainBranch();
|
|
753
|
+
const commitCount = execSync(
|
|
754
|
+
`git rev-list --count ${mainBranch}..HEAD 2>/dev/null || echo 0`,
|
|
755
|
+
{ cwd: sessionPath, encoding: 'utf8' }
|
|
756
|
+
).trim();
|
|
757
|
+
|
|
758
|
+
const commits = parseInt(commitCount, 10);
|
|
759
|
+
|
|
760
|
+
if (commits === 0) {
|
|
761
|
+
return SESSION_PHASES.TODO;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Check for uncommitted changes
|
|
765
|
+
const status = execSync('git status --porcelain 2>/dev/null || echo ""', {
|
|
766
|
+
cwd: sessionPath,
|
|
767
|
+
encoding: 'utf8',
|
|
768
|
+
}).trim();
|
|
769
|
+
|
|
770
|
+
if (status === '') {
|
|
771
|
+
// No uncommitted changes = ready for review
|
|
772
|
+
return SESSION_PHASES.REVIEW;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Has commits but also uncommitted changes = still coding
|
|
776
|
+
return SESSION_PHASES.CODING;
|
|
777
|
+
} catch (e) {
|
|
778
|
+
// On error, assume coding phase
|
|
779
|
+
return SESSION_PHASES.CODING;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Render Kanban-style board visualization
|
|
784
|
+
function renderKanbanBoard(sessions) {
|
|
785
|
+
const lines = [];
|
|
786
|
+
|
|
787
|
+
// Group sessions by phase
|
|
788
|
+
const byPhase = {
|
|
789
|
+
[SESSION_PHASES.TODO]: [],
|
|
790
|
+
[SESSION_PHASES.CODING]: [],
|
|
791
|
+
[SESSION_PHASES.REVIEW]: [],
|
|
792
|
+
[SESSION_PHASES.MERGED]: [],
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
for (const session of sessions) {
|
|
796
|
+
const phase = getSessionPhase(session);
|
|
797
|
+
byPhase[phase].push(session);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Calculate column widths (min 12 chars)
|
|
801
|
+
const colWidth = 14;
|
|
802
|
+
const separator = ' ';
|
|
803
|
+
|
|
804
|
+
// Header
|
|
805
|
+
lines.push(`${c.cyan}Sessions (Kanban View):${c.reset}`);
|
|
806
|
+
lines.push('');
|
|
807
|
+
|
|
808
|
+
// Column headers
|
|
809
|
+
const headers = [
|
|
810
|
+
`${c.dim}TO DO${c.reset}`,
|
|
811
|
+
`${c.yellow}CODING${c.reset}`,
|
|
812
|
+
`${c.blue}REVIEW${c.reset}`,
|
|
813
|
+
`${c.green}MERGED${c.reset}`,
|
|
814
|
+
];
|
|
815
|
+
lines.push(headers.map(h => h.padEnd(colWidth + 10)).join(separator)); // +10 for ANSI codes
|
|
816
|
+
|
|
817
|
+
// Top borders
|
|
818
|
+
const topBorder = `┌${'─'.repeat(colWidth)}┐`;
|
|
819
|
+
lines.push([topBorder, topBorder, topBorder, topBorder].join(separator));
|
|
820
|
+
|
|
821
|
+
// Find max rows needed
|
|
822
|
+
const maxRows = Math.max(
|
|
823
|
+
1,
|
|
824
|
+
byPhase[SESSION_PHASES.TODO].length,
|
|
825
|
+
byPhase[SESSION_PHASES.CODING].length,
|
|
826
|
+
byPhase[SESSION_PHASES.REVIEW].length,
|
|
827
|
+
byPhase[SESSION_PHASES.MERGED].length
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
// Render rows
|
|
831
|
+
for (let i = 0; i < maxRows; i++) {
|
|
832
|
+
const cells = [
|
|
833
|
+
SESSION_PHASES.TODO,
|
|
834
|
+
SESSION_PHASES.CODING,
|
|
835
|
+
SESSION_PHASES.REVIEW,
|
|
836
|
+
SESSION_PHASES.MERGED,
|
|
837
|
+
].map(phase => {
|
|
838
|
+
const session = byPhase[phase][i];
|
|
839
|
+
if (!session) {
|
|
840
|
+
return `│${' '.repeat(colWidth)}│`;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Format session info
|
|
844
|
+
const id = `[${session.id}]`;
|
|
845
|
+
const name = session.nickname || session.branch || '';
|
|
846
|
+
const truncName = name.length > colWidth - 5 ? name.slice(0, colWidth - 8) + '...' : name;
|
|
847
|
+
const content = `${id} ${truncName}`.slice(0, colWidth);
|
|
848
|
+
|
|
849
|
+
return `│${content.padEnd(colWidth)}│`;
|
|
850
|
+
});
|
|
851
|
+
lines.push(cells.join(separator));
|
|
852
|
+
|
|
853
|
+
// Second line with story
|
|
854
|
+
const storyCells = [
|
|
855
|
+
SESSION_PHASES.TODO,
|
|
856
|
+
SESSION_PHASES.CODING,
|
|
857
|
+
SESSION_PHASES.REVIEW,
|
|
858
|
+
SESSION_PHASES.MERGED,
|
|
859
|
+
].map(phase => {
|
|
860
|
+
const session = byPhase[phase][i];
|
|
861
|
+
if (!session) {
|
|
862
|
+
return `│${' '.repeat(colWidth)}│`;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const story = session.story || '-';
|
|
866
|
+
const storyTrunc = story.length > colWidth - 2 ? story.slice(0, colWidth - 5) + '...' : story;
|
|
867
|
+
|
|
868
|
+
return `│${c.dim}${storyTrunc.padEnd(colWidth)}${c.reset}│`;
|
|
869
|
+
});
|
|
870
|
+
lines.push(storyCells.join(separator));
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Bottom borders
|
|
874
|
+
const bottomBorder = `└${'─'.repeat(colWidth)}┘`;
|
|
875
|
+
lines.push([bottomBorder, bottomBorder, bottomBorder, bottomBorder].join(separator));
|
|
876
|
+
|
|
877
|
+
// Summary
|
|
878
|
+
lines.push('');
|
|
879
|
+
const summary = [
|
|
880
|
+
`${c.dim}To Do: ${byPhase[SESSION_PHASES.TODO].length}${c.reset}`,
|
|
881
|
+
`${c.yellow}Coding: ${byPhase[SESSION_PHASES.CODING].length}${c.reset}`,
|
|
882
|
+
`${c.blue}Review: ${byPhase[SESSION_PHASES.REVIEW].length}${c.reset}`,
|
|
883
|
+
`${c.green}Merged: ${byPhase[SESSION_PHASES.MERGED].length}${c.reset}`,
|
|
884
|
+
].join(' │ ');
|
|
885
|
+
lines.push(summary);
|
|
886
|
+
|
|
887
|
+
return lines.join('\n');
|
|
888
|
+
}
|
|
889
|
+
|
|
674
890
|
// Format sessions for display
|
|
675
891
|
function formatSessionsTable(sessions) {
|
|
676
892
|
const lines = [];
|
|
@@ -747,6 +963,11 @@ function main() {
|
|
|
747
963
|
const { sessions, cleaned } = getSessions();
|
|
748
964
|
if (args.includes('--json')) {
|
|
749
965
|
console.log(JSON.stringify({ sessions, cleaned }));
|
|
966
|
+
} else if (args.includes('--kanban')) {
|
|
967
|
+
console.log(renderKanbanBoard(sessions));
|
|
968
|
+
if (cleaned > 0) {
|
|
969
|
+
console.log(`${c.dim}Cleaned ${cleaned} stale lock(s)${c.reset}`);
|
|
970
|
+
}
|
|
750
971
|
} else {
|
|
751
972
|
console.log(formatSessionsTable(sessions));
|
|
752
973
|
if (cleaned > 0) {
|
|
@@ -793,7 +1014,7 @@ function main() {
|
|
|
793
1014
|
|
|
794
1015
|
// Register in single pass (combines register + count + status)
|
|
795
1016
|
const registry = loadRegistry();
|
|
796
|
-
const
|
|
1017
|
+
const cleanupResult = cleanupStaleLocks(registry);
|
|
797
1018
|
const branch = getCurrentBranch();
|
|
798
1019
|
const story = getCurrentStory();
|
|
799
1020
|
const pid = process.ppid || process.pid;
|
|
@@ -814,11 +1035,16 @@ function main() {
|
|
|
814
1035
|
registry.sessions[sessionId].story = story ? story.id : null;
|
|
815
1036
|
registry.sessions[sessionId].last_active = new Date().toISOString();
|
|
816
1037
|
if (nickname) registry.sessions[sessionId].nickname = nickname;
|
|
1038
|
+
// Ensure thread_type exists (migration for old sessions)
|
|
1039
|
+
if (!registry.sessions[sessionId].thread_type) {
|
|
1040
|
+
registry.sessions[sessionId].thread_type = registry.sessions[sessionId].is_main ? 'base' : 'parallel';
|
|
1041
|
+
}
|
|
817
1042
|
writeLock(sessionId, pid);
|
|
818
1043
|
} else {
|
|
819
1044
|
// Create new
|
|
820
1045
|
sessionId = String(registry.next_id);
|
|
821
1046
|
registry.next_id++;
|
|
1047
|
+
const isMain = cwd === ROOT;
|
|
822
1048
|
registry.sessions[sessionId] = {
|
|
823
1049
|
path: cwd,
|
|
824
1050
|
branch,
|
|
@@ -826,7 +1052,8 @@ function main() {
|
|
|
826
1052
|
nickname: nickname || null,
|
|
827
1053
|
created: new Date().toISOString(),
|
|
828
1054
|
last_active: new Date().toISOString(),
|
|
829
|
-
is_main:
|
|
1055
|
+
is_main: isMain,
|
|
1056
|
+
thread_type: isMain ? 'base' : 'parallel',
|
|
830
1057
|
};
|
|
831
1058
|
writeLock(sessionId, pid);
|
|
832
1059
|
isNew = true;
|
|
@@ -853,7 +1080,8 @@ function main() {
|
|
|
853
1080
|
current,
|
|
854
1081
|
otherActive,
|
|
855
1082
|
total: sessions.length,
|
|
856
|
-
cleaned,
|
|
1083
|
+
cleaned: cleanupResult.count,
|
|
1084
|
+
cleanedSessions: cleanupResult.sessions,
|
|
857
1085
|
})
|
|
858
1086
|
);
|
|
859
1087
|
break;
|
|
@@ -984,6 +1212,26 @@ function main() {
|
|
|
984
1212
|
break;
|
|
985
1213
|
}
|
|
986
1214
|
|
|
1215
|
+
case 'thread-type': {
|
|
1216
|
+
const subCommand = args[1];
|
|
1217
|
+
if (subCommand === 'set') {
|
|
1218
|
+
const sessionId = args[2];
|
|
1219
|
+
const threadType = args[3];
|
|
1220
|
+
if (!sessionId || !threadType) {
|
|
1221
|
+
console.log(JSON.stringify({ success: false, error: 'Usage: thread-type set <sessionId> <type>' }));
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const result = setSessionThreadType(sessionId, threadType);
|
|
1225
|
+
console.log(JSON.stringify(result));
|
|
1226
|
+
} else {
|
|
1227
|
+
// Default: get thread type
|
|
1228
|
+
const sessionId = args[1] || null;
|
|
1229
|
+
const result = getSessionThreadType(sessionId);
|
|
1230
|
+
console.log(JSON.stringify(result));
|
|
1231
|
+
}
|
|
1232
|
+
break;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
987
1235
|
case 'help':
|
|
988
1236
|
default:
|
|
989
1237
|
console.log(`
|
|
@@ -1001,6 +1249,8 @@ ${c.cyan}Commands:${c.reset}
|
|
|
1001
1249
|
switch <id|nickname> Switch active session context (for /add-dir)
|
|
1002
1250
|
active Get currently switched session (if any)
|
|
1003
1251
|
clear-active Clear switched session (back to main)
|
|
1252
|
+
thread-type [id] Get thread type for session (default: current)
|
|
1253
|
+
thread-type set <id> <type> Set thread type (base|parallel|chained|fusion|big|long)
|
|
1004
1254
|
check-merge <id> Check if session is mergeable to main
|
|
1005
1255
|
merge-preview <id> Preview commits/files to be merged
|
|
1006
1256
|
integrate <id> [opts] Merge session to main and cleanup
|
|
@@ -1186,7 +1436,7 @@ function smartMerge(sessionId, options = {}) {
|
|
|
1186
1436
|
}
|
|
1187
1437
|
|
|
1188
1438
|
// Categorize and plan resolutions
|
|
1189
|
-
const resolutions = conflictFiles.files.map(
|
|
1439
|
+
const resolutions = conflictFiles.files.map(file => {
|
|
1190
1440
|
const category = categorizeFile(file);
|
|
1191
1441
|
const strategyInfo = getMergeStrategy(category);
|
|
1192
1442
|
return {
|
|
@@ -1295,7 +1545,10 @@ function smartMerge(sessionId, options = {}) {
|
|
|
1295
1545
|
result.worktreeDeleted = true;
|
|
1296
1546
|
} catch (e) {
|
|
1297
1547
|
try {
|
|
1298
|
-
execSync(`git worktree remove --force "${session.path}"`, {
|
|
1548
|
+
execSync(`git worktree remove --force "${session.path}"`, {
|
|
1549
|
+
cwd: ROOT,
|
|
1550
|
+
encoding: 'utf8',
|
|
1551
|
+
});
|
|
1299
1552
|
result.worktreeDeleted = true;
|
|
1300
1553
|
} catch (e2) {
|
|
1301
1554
|
result.worktreeDeleted = false;
|
|
@@ -1395,7 +1648,7 @@ function getConflictingFiles(sessionId) {
|
|
|
1395
1648
|
const branchSet = new Set((branchFiles.stdout || '').trim().split('\n').filter(Boolean));
|
|
1396
1649
|
|
|
1397
1650
|
// Find intersection (files changed in both)
|
|
1398
|
-
const conflicting = [...mainSet].filter(
|
|
1651
|
+
const conflicting = [...mainSet].filter(f => branchSet.has(f));
|
|
1399
1652
|
|
|
1400
1653
|
return { success: true, files: conflicting };
|
|
1401
1654
|
}
|
|
@@ -1612,6 +1865,65 @@ function getActiveSession() {
|
|
|
1612
1865
|
}
|
|
1613
1866
|
}
|
|
1614
1867
|
|
|
1868
|
+
/**
|
|
1869
|
+
* Get thread type for a session.
|
|
1870
|
+
* @param {string} sessionId - Session ID (or null for current session)
|
|
1871
|
+
* @returns {{ success: boolean, thread_type?: string, error?: string }}
|
|
1872
|
+
*/
|
|
1873
|
+
function getSessionThreadType(sessionId = null) {
|
|
1874
|
+
const registry = loadRegistry();
|
|
1875
|
+
const cwd = process.cwd();
|
|
1876
|
+
|
|
1877
|
+
// Find session
|
|
1878
|
+
let targetId = sessionId;
|
|
1879
|
+
if (!targetId) {
|
|
1880
|
+
// Find current session by path
|
|
1881
|
+
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
1882
|
+
if (session.path === cwd) {
|
|
1883
|
+
targetId = id;
|
|
1884
|
+
break;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
if (!targetId || !registry.sessions[targetId]) {
|
|
1890
|
+
return { success: false, error: 'Session not found' };
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
const session = registry.sessions[targetId];
|
|
1894
|
+
// Return thread_type or auto-detect for legacy sessions
|
|
1895
|
+
const threadType = session.thread_type || (session.is_main ? 'base' : 'parallel');
|
|
1896
|
+
|
|
1897
|
+
return {
|
|
1898
|
+
success: true,
|
|
1899
|
+
thread_type: threadType,
|
|
1900
|
+
session_id: targetId,
|
|
1901
|
+
is_main: session.is_main,
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
/**
|
|
1906
|
+
* Update thread type for a session.
|
|
1907
|
+
* @param {string} sessionId - Session ID
|
|
1908
|
+
* @param {string} threadType - New thread type
|
|
1909
|
+
* @returns {{ success: boolean, error?: string }}
|
|
1910
|
+
*/
|
|
1911
|
+
function setSessionThreadType(sessionId, threadType) {
|
|
1912
|
+
if (!THREAD_TYPES.includes(threadType)) {
|
|
1913
|
+
return { success: false, error: `Invalid thread type: ${threadType}. Valid: ${THREAD_TYPES.join(', ')}` };
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
const registry = loadRegistry();
|
|
1917
|
+
if (!registry.sessions[sessionId]) {
|
|
1918
|
+
return { success: false, error: `Session ${sessionId} not found` };
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
registry.sessions[sessionId].thread_type = threadType;
|
|
1922
|
+
saveRegistry(registry);
|
|
1923
|
+
|
|
1924
|
+
return { success: true, thread_type: threadType };
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1615
1927
|
// Export for use as module
|
|
1616
1928
|
module.exports = {
|
|
1617
1929
|
loadRegistry,
|
|
@@ -1639,6 +1951,15 @@ module.exports = {
|
|
|
1639
1951
|
switchSession,
|
|
1640
1952
|
clearActiveSession,
|
|
1641
1953
|
getActiveSession,
|
|
1954
|
+
// Thread type tracking
|
|
1955
|
+
THREAD_TYPES,
|
|
1956
|
+
detectThreadType,
|
|
1957
|
+
getSessionThreadType,
|
|
1958
|
+
setSessionThreadType,
|
|
1959
|
+
// Kanban visualization
|
|
1960
|
+
SESSION_PHASES,
|
|
1961
|
+
getSessionPhase,
|
|
1962
|
+
renderKanbanBoard,
|
|
1642
1963
|
};
|
|
1643
1964
|
|
|
1644
1965
|
// Run CLI if executed directly
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* test-session-boundary.js - Test session boundary hook logic
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node test-session-boundary.js --active=/path/to/session --file=/path/to/file
|
|
7
|
+
*
|
|
8
|
+
* Examples:
|
|
9
|
+
* node test-session-boundary.js --active=/home/coder/project-bugfix --file=/home/coder/project-bugfix/src/App.tsx
|
|
10
|
+
* → ALLOWED (file is inside active session)
|
|
11
|
+
*
|
|
12
|
+
* node test-session-boundary.js --active=/home/coder/project-bugfix --file=/home/coder/project/src/App.tsx
|
|
13
|
+
* → BLOCKED (file is outside active session)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
// Parse arguments
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
let activeSessionPath = null;
|
|
21
|
+
let filePath = null;
|
|
22
|
+
|
|
23
|
+
for (const arg of args) {
|
|
24
|
+
if (arg.startsWith('--active=')) {
|
|
25
|
+
activeSessionPath = arg.slice('--active='.length);
|
|
26
|
+
} else if (arg.startsWith('--file=')) {
|
|
27
|
+
filePath = arg.slice('--file='.length);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Show usage if missing args
|
|
32
|
+
if (!activeSessionPath || !filePath) {
|
|
33
|
+
console.log(`
|
|
34
|
+
Session Boundary Hook Tester
|
|
35
|
+
|
|
36
|
+
Usage:
|
|
37
|
+
node test-session-boundary.js --active=<session_path> --file=<file_path>
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
# File INSIDE active session (should be ALLOWED)
|
|
41
|
+
node test-session-boundary.js \\
|
|
42
|
+
--active=/home/coder/project-bugfix \\
|
|
43
|
+
--file=/home/coder/project-bugfix/src/App.tsx
|
|
44
|
+
|
|
45
|
+
# File OUTSIDE active session (should be BLOCKED)
|
|
46
|
+
node test-session-boundary.js \\
|
|
47
|
+
--active=/home/coder/project-bugfix \\
|
|
48
|
+
--file=/home/coder/project/src/App.tsx
|
|
49
|
+
`);
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Normalize paths
|
|
54
|
+
const normalizedActive = path.resolve(activeSessionPath);
|
|
55
|
+
const normalizedFile = path.resolve(filePath);
|
|
56
|
+
|
|
57
|
+
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
58
|
+
console.log('Session Boundary Check');
|
|
59
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
60
|
+
|
|
61
|
+
console.log(`Active Session Path: ${normalizedActive}`);
|
|
62
|
+
console.log(`File Being Edited: ${normalizedFile}`);
|
|
63
|
+
console.log('');
|
|
64
|
+
|
|
65
|
+
// Check if file is within active session path
|
|
66
|
+
const isInsideSession = normalizedFile.startsWith(normalizedActive + path.sep) ||
|
|
67
|
+
normalizedFile === normalizedActive;
|
|
68
|
+
|
|
69
|
+
if (isInsideSession) {
|
|
70
|
+
console.log('✅ ALLOWED - File is inside the active session directory');
|
|
71
|
+
console.log('');
|
|
72
|
+
console.log('The hook would exit(0) and allow this edit.');
|
|
73
|
+
} else {
|
|
74
|
+
console.log('❌ BLOCKED - File is OUTSIDE the active session directory!');
|
|
75
|
+
console.log('');
|
|
76
|
+
console.log('The hook would exit(2) and block this edit with message:');
|
|
77
|
+
console.log(` "Edit blocked: ${normalizedFile} is outside active session ${normalizedActive}"`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
@@ -447,8 +447,46 @@ Before implementing, evaluate task complexity:
|
|
|
447
447
|
3. Design implementation approach
|
|
448
448
|
4. Present plan with file paths and steps
|
|
449
449
|
5. Clarify decisions with user
|
|
450
|
-
6.
|
|
451
|
-
7.
|
|
450
|
+
6. **PLAN REVIEW CHECKPOINT** (see below)
|
|
451
|
+
7. Get approval → `ExitPlanMode`
|
|
452
|
+
8. Implement the approved plan
|
|
453
|
+
|
|
454
|
+
### Plan Review Checkpoint (CRITICAL)
|
|
455
|
+
|
|
456
|
+
**Before transitioning from plan → implement, ALWAYS display:**
|
|
457
|
+
|
|
458
|
+
```markdown
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
## Plan Review Checkpoint
|
|
462
|
+
|
|
463
|
+
**Leverage Reminder:**
|
|
464
|
+
```
|
|
465
|
+
Bad line of code = 1 bad line
|
|
466
|
+
Bad part of plan = 100+ bad lines
|
|
467
|
+
Bad line of research = entire direction is hosed
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Take time to review this plan. A few minutes now saves hours later.
|
|
471
|
+
|
|
472
|
+
**Review Checklist:**
|
|
473
|
+
- [ ] Does this approach make sense for the codebase?
|
|
474
|
+
- [ ] Are there simpler alternatives?
|
|
475
|
+
- [ ] Will this cause breaking changes?
|
|
476
|
+
- [ ] Are edge cases covered?
|
|
477
|
+
|
|
478
|
+
**Approve plan and proceed to implementation?**
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**Why this matters:**
|
|
482
|
+
- Plans are the highest-leverage checkpoint for human review
|
|
483
|
+
- Catching a bad approach before coding saves 100s of lines of rework
|
|
484
|
+
- This is where to invest review time (not code review)
|
|
485
|
+
|
|
486
|
+
**Options to present:**
|
|
487
|
+
1. **Approve** - Proceed to implementation
|
|
488
|
+
2. **Iterate** - Modify the plan first
|
|
489
|
+
3. **Research** - Need more context before deciding
|
|
452
490
|
|
|
453
491
|
**Plan Quality Checklist**:
|
|
454
492
|
- [ ] Explored relevant codebase
|