agileflow 2.85.0 → 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 +5 -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 +11 -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 +125 -14
- package/scripts/session-boundary.js +3 -3
- package/scripts/session-manager.js +303 -8
- 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 +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
|
@@ -26,8 +26,67 @@ const { isValidCommandName } = require('../lib/validate');
|
|
|
26
26
|
// Summary table should be the LAST thing visible before truncation.
|
|
27
27
|
const DISPLAY_LIMIT = 29200;
|
|
28
28
|
|
|
29
|
-
//
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// Progressive Disclosure: Section Activation
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse command-line arguments and determine which sections to activate.
|
|
35
|
+
* Sections are conditionally loaded based on parameters like MODE=loop.
|
|
36
|
+
*
|
|
37
|
+
* Section mapping:
|
|
38
|
+
* - MODE=loop → activates: loop-mode
|
|
39
|
+
* - Multi-session env → activates: multi-session
|
|
40
|
+
* - (Other triggers detected at runtime by the agent)
|
|
41
|
+
*
|
|
42
|
+
* @param {string[]} args - Command-line arguments after command name
|
|
43
|
+
* @returns {Object} { activeSections: string[], params: Object }
|
|
44
|
+
*/
|
|
45
|
+
function parseCommandArgs(args) {
|
|
46
|
+
const activeSections = [];
|
|
47
|
+
const params = {};
|
|
48
|
+
|
|
49
|
+
for (const arg of args) {
|
|
50
|
+
// Parse KEY=VALUE arguments
|
|
51
|
+
const match = arg.match(/^([A-Z_]+)=(.+)$/i);
|
|
52
|
+
if (match) {
|
|
53
|
+
const [, key, value] = match;
|
|
54
|
+
params[key.toUpperCase()] = value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Activate sections based on parameters
|
|
59
|
+
if (params.MODE === 'loop') {
|
|
60
|
+
activeSections.push('loop-mode');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (params.VISUAL === 'true') {
|
|
64
|
+
activeSections.push('visual-e2e');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check for multi-session environment
|
|
68
|
+
const registryPath = '.agileflow/sessions/registry.json';
|
|
69
|
+
if (fs.existsSync(registryPath)) {
|
|
70
|
+
try {
|
|
71
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
72
|
+
const sessionCount = Object.keys(registry.sessions || {}).length;
|
|
73
|
+
if (sessionCount > 1) {
|
|
74
|
+
activeSections.push('multi-session');
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// Silently ignore registry read errors
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { activeSections, params };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Parse arguments
|
|
30
85
|
const commandName = process.argv[2];
|
|
86
|
+
const commandArgs = process.argv.slice(3);
|
|
87
|
+
const { activeSections, params: commandParams } = parseCommandArgs(commandArgs);
|
|
88
|
+
|
|
89
|
+
// Register command for PreCompact context preservation
|
|
31
90
|
if (commandName && isValidCommandName(commandName)) {
|
|
32
91
|
const sessionStatePath = 'docs/09-agents/session-state.json';
|
|
33
92
|
if (fs.existsSync(sessionStatePath)) {
|
|
@@ -42,11 +101,13 @@ if (commandName && isValidCommandName(commandName)) {
|
|
|
42
101
|
// Remove any existing entry for this command (avoid duplicates)
|
|
43
102
|
state.active_commands = state.active_commands.filter(c => c.name !== commandName);
|
|
44
103
|
|
|
45
|
-
// Add the new command
|
|
104
|
+
// Add the new command with active sections for progressive disclosure
|
|
46
105
|
state.active_commands.push({
|
|
47
106
|
name: commandName,
|
|
48
107
|
activated_at: new Date().toISOString(),
|
|
49
108
|
state: {},
|
|
109
|
+
active_sections: activeSections,
|
|
110
|
+
params: commandParams,
|
|
50
111
|
});
|
|
51
112
|
|
|
52
113
|
// Remove legacy active_command field (only use active_commands array now)
|
|
@@ -262,6 +323,13 @@ function generateSummary() {
|
|
|
262
323
|
summary += row('⭐ Up Next', readyStories.slice(0, 3).join(', '), C.skyBlue, C.skyBlue);
|
|
263
324
|
}
|
|
264
325
|
|
|
326
|
+
// Progressive disclosure: Show active sections
|
|
327
|
+
if (activeSections.length > 0) {
|
|
328
|
+
summary += divider();
|
|
329
|
+
const sectionList = activeSections.join(', ');
|
|
330
|
+
summary += row('📖 Sections', sectionList, C.cyan, C.mintGreen);
|
|
331
|
+
}
|
|
332
|
+
|
|
265
333
|
summary += divider();
|
|
266
334
|
|
|
267
335
|
// Key files (using vibrant 256-color palette)
|
|
@@ -318,6 +386,35 @@ function generateFullContent() {
|
|
|
318
386
|
content += `${C.lavender}${C.bold}${title}${C.reset}\n`;
|
|
319
387
|
content += `${C.dim}Generated: ${new Date().toISOString()}${C.reset}\n`;
|
|
320
388
|
|
|
389
|
+
// 0. PROGRESSIVE DISCLOSURE (section activation)
|
|
390
|
+
if (activeSections.length > 0) {
|
|
391
|
+
content += `\n${C.cyan}${C.bold}═══ 📖 Progressive Disclosure: Active Sections ═══${C.reset}\n`;
|
|
392
|
+
content += `${C.dim}The following sections are activated based on command parameters.${C.reset}\n`;
|
|
393
|
+
content += `${C.dim}Look for <!-- SECTION: name --> markers in the command file.${C.reset}\n\n`;
|
|
394
|
+
|
|
395
|
+
activeSections.forEach(section => {
|
|
396
|
+
content += ` ${C.mintGreen}✓${C.reset} ${C.bold}${section}${C.reset}\n`;
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Map sections to their triggers for context
|
|
400
|
+
const sectionDescriptions = {
|
|
401
|
+
'loop-mode': 'Autonomous epic execution (MODE=loop)',
|
|
402
|
+
'multi-session': 'Multi-session coordination detected',
|
|
403
|
+
'visual-e2e': 'Visual screenshot verification (VISUAL=true)',
|
|
404
|
+
'delegation': 'Expert spawning patterns (load when spawning)',
|
|
405
|
+
'stuck': 'Research prompt guidance (load after 2 failures)',
|
|
406
|
+
'plan-mode': 'Planning workflow details (load when entering plan mode)',
|
|
407
|
+
'tools': 'Tool usage guidance (load when needed)',
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
content += `\n${C.dim}Section meanings:${C.reset}\n`;
|
|
411
|
+
activeSections.forEach(section => {
|
|
412
|
+
const desc = sectionDescriptions[section] || 'Conditional content';
|
|
413
|
+
content += ` ${C.dim}• ${section}: ${desc}${C.reset}\n`;
|
|
414
|
+
});
|
|
415
|
+
content += '\n';
|
|
416
|
+
}
|
|
417
|
+
|
|
321
418
|
// 1. GIT STATUS (using vibrant 256-color palette)
|
|
322
419
|
content += `\n${C.skyBlue}${C.bold}═══ Git Status ═══${C.reset}\n`;
|
|
323
420
|
const branch = safeExec('git branch --show-current') || 'unknown';
|
|
@@ -376,6 +473,19 @@ function generateFullContent() {
|
|
|
376
473
|
// Backwards compatibility for old format
|
|
377
474
|
content += `Active command: ${C.skyBlue}${sessionState.active_command.name}${C.reset}\n`;
|
|
378
475
|
}
|
|
476
|
+
|
|
477
|
+
// Show batch loop status if active
|
|
478
|
+
const batchLoop = sessionState.batch_loop;
|
|
479
|
+
if (batchLoop && batchLoop.enabled) {
|
|
480
|
+
content += `\n${C.skyBlue}${C.bold}── Batch Loop Active ──${C.reset}\n`;
|
|
481
|
+
content += `Pattern: ${C.cyan}${batchLoop.pattern}${C.reset}\n`;
|
|
482
|
+
content += `Action: ${C.cyan}${batchLoop.action}${C.reset}\n`;
|
|
483
|
+
content += `Current: ${C.lightYellow}${batchLoop.current_item || 'none'}${C.reset}\n`;
|
|
484
|
+
const summary = batchLoop.summary || {};
|
|
485
|
+
content += `Progress: ${C.lightGreen}${summary.completed || 0}${C.reset}/${summary.total || 0} `;
|
|
486
|
+
content += `(${C.lightYellow}${summary.in_progress || 0}${C.reset} in progress)\n`;
|
|
487
|
+
content += `Iteration: ${batchLoop.iteration || 0}/${batchLoop.max_iterations || 50}\n`;
|
|
488
|
+
}
|
|
379
489
|
} else {
|
|
380
490
|
content += `${C.dim}No session-state.json found${C.reset}\n`;
|
|
381
491
|
}
|
|
@@ -440,9 +550,7 @@ function generateFullContent() {
|
|
|
440
550
|
|
|
441
551
|
if (fs.existsSync(storyClaimingPath) || fs.existsSync(altStoryClaimingPath)) {
|
|
442
552
|
try {
|
|
443
|
-
const claimPath = fs.existsSync(storyClaimingPath)
|
|
444
|
-
? storyClaimingPath
|
|
445
|
-
: altStoryClaimingPath;
|
|
553
|
+
const claimPath = fs.existsSync(storyClaimingPath) ? storyClaimingPath : altStoryClaimingPath;
|
|
446
554
|
const storyClaiming = require(claimPath);
|
|
447
555
|
|
|
448
556
|
// Get stories claimed by other sessions
|
|
@@ -451,7 +559,9 @@ function generateFullContent() {
|
|
|
451
559
|
content += `\n${C.amber}${C.bold}═══ 🔒 Claimed Stories ═══${C.reset}\n`;
|
|
452
560
|
content += `${C.dim}Stories locked by other sessions - pick a different one${C.reset}\n`;
|
|
453
561
|
othersResult.stories.forEach(story => {
|
|
454
|
-
const sessionDir = story.claimedBy?.path
|
|
562
|
+
const sessionDir = story.claimedBy?.path
|
|
563
|
+
? path.basename(story.claimedBy.path)
|
|
564
|
+
: 'unknown';
|
|
455
565
|
content += ` ${C.coral}🔒${C.reset} ${C.lavender}${story.id}${C.reset} "${story.title}" ${C.dim}→ Session ${story.claimedBy?.session_id || '?'} (${sessionDir})${C.reset}\n`;
|
|
456
566
|
});
|
|
457
567
|
content += '\n';
|
|
@@ -477,9 +587,7 @@ function generateFullContent() {
|
|
|
477
587
|
|
|
478
588
|
if (fs.existsSync(fileTrackingPath) || fs.existsSync(altFileTrackingPath)) {
|
|
479
589
|
try {
|
|
480
|
-
const trackPath = fs.existsSync(fileTrackingPath)
|
|
481
|
-
? fileTrackingPath
|
|
482
|
-
: altFileTrackingPath;
|
|
590
|
+
const trackPath = fs.existsSync(fileTrackingPath) ? fileTrackingPath : altFileTrackingPath;
|
|
483
591
|
const fileTracking = require(trackPath);
|
|
484
592
|
|
|
485
593
|
// Get file overlaps with other sessions
|
|
@@ -488,10 +596,12 @@ function generateFullContent() {
|
|
|
488
596
|
content += `\n${C.amber}${C.bold}═══ ⚠️ File Overlaps ═══${C.reset}\n`;
|
|
489
597
|
content += `${C.dim}Files also edited by other sessions - conflicts auto-resolved during merge${C.reset}\n`;
|
|
490
598
|
overlapsResult.overlaps.forEach(overlap => {
|
|
491
|
-
const sessionInfo = overlap.otherSessions
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
599
|
+
const sessionInfo = overlap.otherSessions
|
|
600
|
+
.map(s => {
|
|
601
|
+
const dir = path.basename(s.path);
|
|
602
|
+
return `Session ${s.id} (${dir})`;
|
|
603
|
+
})
|
|
604
|
+
.join(', ');
|
|
495
605
|
content += ` ${C.amber}⚠${C.reset} ${C.lavender}${overlap.file}${C.reset} ${C.dim}→ ${sessionInfo}${C.reset}\n`;
|
|
496
606
|
});
|
|
497
607
|
content += '\n';
|
|
@@ -524,7 +634,8 @@ function generateFullContent() {
|
|
|
524
634
|
// 6. VISUAL E2E STATUS (detect from metadata or filesystem)
|
|
525
635
|
const metadata = safeReadJSON('docs/00-meta/agileflow-metadata.json');
|
|
526
636
|
const visualE2eConfig = metadata?.features?.visual_e2e;
|
|
527
|
-
const playwrightExists =
|
|
637
|
+
const playwrightExists =
|
|
638
|
+
fs.existsSync('playwright.config.ts') || fs.existsSync('playwright.config.js');
|
|
528
639
|
const screenshotsExists = fs.existsSync('screenshots');
|
|
529
640
|
const testsE2eExists = fs.existsSync('tests/e2e');
|
|
530
641
|
|
|
@@ -61,8 +61,9 @@ function isInsideSession(filePath, sessionPath) {
|
|
|
61
61
|
const normalizedFile = path.resolve(filePath);
|
|
62
62
|
const normalizedSession = path.resolve(sessionPath);
|
|
63
63
|
|
|
64
|
-
return
|
|
65
|
-
|
|
64
|
+
return (
|
|
65
|
+
normalizedFile.startsWith(normalizedSession + path.sep) || normalizedFile === normalizedSession
|
|
66
|
+
);
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
// Output blocked message
|
|
@@ -117,7 +118,6 @@ function main() {
|
|
|
117
118
|
// File is OUTSIDE active session - BLOCK
|
|
118
119
|
outputBlocked(filePath, activeSession);
|
|
119
120
|
process.exit(2);
|
|
120
|
-
|
|
121
121
|
} catch (e) {
|
|
122
122
|
// Parse error or other issue - fail open
|
|
123
123
|
process.exit(0);
|
|
@@ -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) {
|
|
@@ -814,11 +1010,16 @@ function main() {
|
|
|
814
1010
|
registry.sessions[sessionId].story = story ? story.id : null;
|
|
815
1011
|
registry.sessions[sessionId].last_active = new Date().toISOString();
|
|
816
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
|
+
}
|
|
817
1017
|
writeLock(sessionId, pid);
|
|
818
1018
|
} else {
|
|
819
1019
|
// Create new
|
|
820
1020
|
sessionId = String(registry.next_id);
|
|
821
1021
|
registry.next_id++;
|
|
1022
|
+
const isMain = cwd === ROOT;
|
|
822
1023
|
registry.sessions[sessionId] = {
|
|
823
1024
|
path: cwd,
|
|
824
1025
|
branch,
|
|
@@ -826,7 +1027,8 @@ function main() {
|
|
|
826
1027
|
nickname: nickname || null,
|
|
827
1028
|
created: new Date().toISOString(),
|
|
828
1029
|
last_active: new Date().toISOString(),
|
|
829
|
-
is_main:
|
|
1030
|
+
is_main: isMain,
|
|
1031
|
+
thread_type: isMain ? 'base' : 'parallel',
|
|
830
1032
|
};
|
|
831
1033
|
writeLock(sessionId, pid);
|
|
832
1034
|
isNew = true;
|
|
@@ -984,6 +1186,26 @@ function main() {
|
|
|
984
1186
|
break;
|
|
985
1187
|
}
|
|
986
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
|
+
|
|
987
1209
|
case 'help':
|
|
988
1210
|
default:
|
|
989
1211
|
console.log(`
|
|
@@ -1001,6 +1223,8 @@ ${c.cyan}Commands:${c.reset}
|
|
|
1001
1223
|
switch <id|nickname> Switch active session context (for /add-dir)
|
|
1002
1224
|
active Get currently switched session (if any)
|
|
1003
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)
|
|
1004
1228
|
check-merge <id> Check if session is mergeable to main
|
|
1005
1229
|
merge-preview <id> Preview commits/files to be merged
|
|
1006
1230
|
integrate <id> [opts] Merge session to main and cleanup
|
|
@@ -1186,7 +1410,7 @@ function smartMerge(sessionId, options = {}) {
|
|
|
1186
1410
|
}
|
|
1187
1411
|
|
|
1188
1412
|
// Categorize and plan resolutions
|
|
1189
|
-
const resolutions = conflictFiles.files.map(
|
|
1413
|
+
const resolutions = conflictFiles.files.map(file => {
|
|
1190
1414
|
const category = categorizeFile(file);
|
|
1191
1415
|
const strategyInfo = getMergeStrategy(category);
|
|
1192
1416
|
return {
|
|
@@ -1295,7 +1519,10 @@ function smartMerge(sessionId, options = {}) {
|
|
|
1295
1519
|
result.worktreeDeleted = true;
|
|
1296
1520
|
} catch (e) {
|
|
1297
1521
|
try {
|
|
1298
|
-
execSync(`git worktree remove --force "${session.path}"`, {
|
|
1522
|
+
execSync(`git worktree remove --force "${session.path}"`, {
|
|
1523
|
+
cwd: ROOT,
|
|
1524
|
+
encoding: 'utf8',
|
|
1525
|
+
});
|
|
1299
1526
|
result.worktreeDeleted = true;
|
|
1300
1527
|
} catch (e2) {
|
|
1301
1528
|
result.worktreeDeleted = false;
|
|
@@ -1395,7 +1622,7 @@ function getConflictingFiles(sessionId) {
|
|
|
1395
1622
|
const branchSet = new Set((branchFiles.stdout || '').trim().split('\n').filter(Boolean));
|
|
1396
1623
|
|
|
1397
1624
|
// Find intersection (files changed in both)
|
|
1398
|
-
const conflicting = [...mainSet].filter(
|
|
1625
|
+
const conflicting = [...mainSet].filter(f => branchSet.has(f));
|
|
1399
1626
|
|
|
1400
1627
|
return { success: true, files: conflicting };
|
|
1401
1628
|
}
|
|
@@ -1612,6 +1839,65 @@ function getActiveSession() {
|
|
|
1612
1839
|
}
|
|
1613
1840
|
}
|
|
1614
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
|
+
|
|
1615
1901
|
// Export for use as module
|
|
1616
1902
|
module.exports = {
|
|
1617
1903
|
loadRegistry,
|
|
@@ -1639,6 +1925,15 @@ module.exports = {
|
|
|
1639
1925
|
switchSession,
|
|
1640
1926
|
clearActiveSession,
|
|
1641
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,
|
|
1642
1937
|
};
|
|
1643
1938
|
|
|
1644
1939
|
// Run CLI if executed directly
|