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.
@@ -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
- // Optional: Register command for PreCompact context preservation
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,96 @@ 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.5 SESSION CONTEXT BANNER (FIRST - before everything else)
390
+ // This is critical for multi-session awareness - agents need to know which session they're in
391
+ const sessionManagerPath = path.join(__dirname, 'session-manager.js');
392
+ const altSessionManagerPath = '.agileflow/scripts/session-manager.js';
393
+
394
+ if (fs.existsSync(sessionManagerPath) || fs.existsSync(altSessionManagerPath)) {
395
+ const managerPath = fs.existsSync(sessionManagerPath)
396
+ ? sessionManagerPath
397
+ : altSessionManagerPath;
398
+ const sessionStatus = safeExec(`node "${managerPath}" status`);
399
+
400
+ if (sessionStatus) {
401
+ try {
402
+ const statusData = JSON.parse(sessionStatus);
403
+ if (statusData.current) {
404
+ const session = statusData.current;
405
+ const isMain = session.is_main === true;
406
+ const sessionName = session.nickname
407
+ ? `Session ${session.id} "${session.nickname}"`
408
+ : `Session ${session.id}`;
409
+
410
+ content += `\n${C.teal}${C.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}\n`;
411
+ content += `${C.teal}${C.bold}📍 SESSION CONTEXT${C.reset}\n`;
412
+ content += `${C.teal}${C.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}\n`;
413
+
414
+ if (isMain) {
415
+ content += `${C.mintGreen}${C.bold}${sessionName}${C.reset} ${C.dim}(main project)${C.reset}\n`;
416
+ } else {
417
+ content += `${C.peach}${C.bold}🔀 ${sessionName}${C.reset} ${C.dim}(worktree)${C.reset}\n`;
418
+ content += `Branch: ${C.skyBlue}${session.branch || 'unknown'}${C.reset}\n`;
419
+ content += `${C.dim}Path: ${session.path || process.cwd()}${C.reset}\n`;
420
+ }
421
+
422
+ // Show other active sessions prominently
423
+ if (statusData.otherActive > 0) {
424
+ content += `${C.amber}⚠️ ${statusData.otherActive} other active session(s)${C.reset} - check story claims below\n`;
425
+ }
426
+
427
+ content += `${C.teal}${C.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}\n\n`;
428
+ }
429
+ } catch (e) {
430
+ // Silently ignore session parse errors - will still show detailed session context later
431
+ }
432
+ }
433
+ }
434
+
435
+ // 0.7 INTERACTION MODE (AskUserQuestion) - EARLY for visibility
436
+ // This MUST appear before other content to ensure Claude sees it
437
+ const earlyMetadata = safeReadJSON('docs/00-meta/agileflow-metadata.json');
438
+ const askUserQuestionConfig = earlyMetadata?.features?.askUserQuestion;
439
+
440
+ if (askUserQuestionConfig?.enabled) {
441
+ content += `${C.coral}${C.bold}┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓${C.reset}\n`;
442
+ content += `${C.coral}${C.bold}┃ 🔔 MANDATORY: AskUserQuestion After EVERY Response ┃${C.reset}\n`;
443
+ content += `${C.coral}${C.bold}┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛${C.reset}\n`;
444
+ content += `${C.bold}After completing ANY task${C.reset} (implementation, fix, etc.):\n`;
445
+ content += `${C.mintGreen}→ ALWAYS${C.reset} call ${C.skyBlue}AskUserQuestion${C.reset} tool to offer next steps\n`;
446
+ content += `${C.coral}→ NEVER${C.reset} end with text like "Done!" or "What's next?"\n\n`;
447
+ content += `${C.dim}Balance: Use at natural pause points. Don't ask permission for routine work.${C.reset}\n\n`;
448
+ }
449
+
450
+ // 0. PROGRESSIVE DISCLOSURE (section activation)
451
+ if (activeSections.length > 0) {
452
+ content += `\n${C.cyan}${C.bold}═══ 📖 Progressive Disclosure: Active Sections ═══${C.reset}\n`;
453
+ content += `${C.dim}The following sections are activated based on command parameters.${C.reset}\n`;
454
+ content += `${C.dim}Look for <!-- SECTION: name --> markers in the command file.${C.reset}\n\n`;
455
+
456
+ activeSections.forEach(section => {
457
+ content += ` ${C.mintGreen}✓${C.reset} ${C.bold}${section}${C.reset}\n`;
458
+ });
459
+
460
+ // Map sections to their triggers for context
461
+ const sectionDescriptions = {
462
+ 'loop-mode': 'Autonomous epic execution (MODE=loop)',
463
+ 'multi-session': 'Multi-session coordination detected',
464
+ 'visual-e2e': 'Visual screenshot verification (VISUAL=true)',
465
+ 'delegation': 'Expert spawning patterns (load when spawning)',
466
+ 'stuck': 'Research prompt guidance (load after 2 failures)',
467
+ 'plan-mode': 'Planning workflow details (load when entering plan mode)',
468
+ 'tools': 'Tool usage guidance (load when needed)',
469
+ };
470
+
471
+ content += `\n${C.dim}Section meanings:${C.reset}\n`;
472
+ activeSections.forEach(section => {
473
+ const desc = sectionDescriptions[section] || 'Conditional content';
474
+ content += ` ${C.dim}• ${section}: ${desc}${C.reset}\n`;
475
+ });
476
+ content += '\n';
477
+ }
478
+
321
479
  // 1. GIT STATUS (using vibrant 256-color palette)
322
480
  content += `\n${C.skyBlue}${C.bold}═══ Git Status ═══${C.reset}\n`;
323
481
  const branch = safeExec('git branch --show-current') || 'unknown';
@@ -376,62 +534,52 @@ function generateFullContent() {
376
534
  // Backwards compatibility for old format
377
535
  content += `Active command: ${C.skyBlue}${sessionState.active_command.name}${C.reset}\n`;
378
536
  }
537
+
538
+ // Show batch loop status if active
539
+ const batchLoop = sessionState.batch_loop;
540
+ if (batchLoop && batchLoop.enabled) {
541
+ content += `\n${C.skyBlue}${C.bold}── Batch Loop Active ──${C.reset}\n`;
542
+ content += `Pattern: ${C.cyan}${batchLoop.pattern}${C.reset}\n`;
543
+ content += `Action: ${C.cyan}${batchLoop.action}${C.reset}\n`;
544
+ content += `Current: ${C.lightYellow}${batchLoop.current_item || 'none'}${C.reset}\n`;
545
+ const summary = batchLoop.summary || {};
546
+ content += `Progress: ${C.lightGreen}${summary.completed || 0}${C.reset}/${summary.total || 0} `;
547
+ content += `(${C.lightYellow}${summary.in_progress || 0}${C.reset} in progress)\n`;
548
+ content += `Iteration: ${batchLoop.iteration || 0}/${batchLoop.max_iterations || 50}\n`;
549
+ }
379
550
  } else {
380
551
  content += `${C.dim}No session-state.json found${C.reset}\n`;
381
552
  }
382
553
 
383
- // 4. SESSION CONTEXT (multi-session awareness)
384
- content += `\n${C.skyBlue}${C.bold}═══ Session Context ═══${C.reset}\n`;
385
- const sessionManagerPath = path.join(__dirname, 'session-manager.js');
386
- const altSessionManagerPath = '.agileflow/scripts/session-manager.js';
554
+ // 4. SESSION CONTEXT (details - banner shown above)
555
+ // Note: Prominent SESSION CONTEXT banner is shown at the top of output
556
+ // This section provides additional details for non-main sessions
557
+ const sessionMgrPath = path.join(__dirname, 'session-manager.js');
558
+ const altSessionMgrPath = '.agileflow/scripts/session-manager.js';
387
559
 
388
- if (fs.existsSync(sessionManagerPath) || fs.existsSync(altSessionManagerPath)) {
389
- const managerPath = fs.existsSync(sessionManagerPath)
390
- ? sessionManagerPath
391
- : altSessionManagerPath;
392
- const sessionStatus = safeExec(`node "${managerPath}" status`);
560
+ if (fs.existsSync(sessionMgrPath) || fs.existsSync(altSessionMgrPath)) {
561
+ const mgrPath = fs.existsSync(sessionMgrPath) ? sessionMgrPath : altSessionMgrPath;
562
+ const sessionStatusStr = safeExec(`node "${mgrPath}" status`);
393
563
 
394
- if (sessionStatus) {
564
+ if (sessionStatusStr) {
395
565
  try {
396
- const statusData = JSON.parse(sessionStatus);
397
- if (statusData.current) {
566
+ const statusData = JSON.parse(sessionStatusStr);
567
+ if (statusData.current && !statusData.current.is_main) {
568
+ // Only show additional details for non-main sessions
569
+ content += `\n${C.skyBlue}${C.bold}═══ Session Details ═══${C.reset}\n`;
398
570
  const session = statusData.current;
399
- const isMain = session.is_main === true;
400
-
401
- if (isMain) {
402
- content += `Session: ${C.mintGreen}Main project${C.reset} (Session ${session.id || 1})\n`;
403
- } else {
404
- // NON-MAIN SESSION - Show prominent banner
405
- const sessionName = session.nickname
406
- ? `${session.id} "${session.nickname}"`
407
- : `${session.id}`;
408
- content += `${C.teal}${C.bold}🔀 SESSION ${sessionName} (worktree)${C.reset}\n`;
409
- content += `Branch: ${C.skyBlue}${session.branch || 'unknown'}${C.reset}\n`;
410
- content += `Path: ${C.dim}${session.path || process.cwd()}${C.reset}\n`;
411
571
 
412
- // Calculate relative path to main
413
- const mainPath = process.cwd().replace(/-[^/]+$/, ''); // Heuristic: strip session suffix
414
- content += `Main project: ${C.dim}${mainPath}${C.reset}\n`;
572
+ // Calculate relative path to main
573
+ const mainPath = process.cwd().replace(/-[^/]+$/, ''); // Heuristic: strip session suffix
574
+ content += `Main project: ${C.dim}${mainPath}${C.reset}\n`;
415
575
 
416
- // Remind about merge flow
417
- content += `${C.lavender}💡 When done: /agileflow:session:end → merge to main${C.reset}\n`;
418
- }
419
-
420
- // Show other active sessions
421
- if (statusData.otherActive > 0) {
422
- content += `${C.peach}⚠️ ${statusData.otherActive} other session(s) active${C.reset}\n`;
423
- }
424
- } else {
425
- content += `${C.dim}No session registered${C.reset}\n`;
576
+ // Remind about merge flow
577
+ content += `${C.lavender}💡 When done: /agileflow:session:end → merge to main${C.reset}\n`;
426
578
  }
427
579
  } catch (e) {
428
- content += `${C.dim}Session manager available but status parse failed${C.reset}\n`;
580
+ // Silently ignore - banner above has basic info
429
581
  }
430
- } else {
431
- content += `${C.dim}Session manager available${C.reset}\n`;
432
582
  }
433
- } else {
434
- content += `${C.dim}Multi-session not configured${C.reset}\n`;
435
583
  }
436
584
 
437
585
  // 5. STORY CLAIMS (inter-session coordination)
@@ -440,9 +588,7 @@ function generateFullContent() {
440
588
 
441
589
  if (fs.existsSync(storyClaimingPath) || fs.existsSync(altStoryClaimingPath)) {
442
590
  try {
443
- const claimPath = fs.existsSync(storyClaimingPath)
444
- ? storyClaimingPath
445
- : altStoryClaimingPath;
591
+ const claimPath = fs.existsSync(storyClaimingPath) ? storyClaimingPath : altStoryClaimingPath;
446
592
  const storyClaiming = require(claimPath);
447
593
 
448
594
  // Get stories claimed by other sessions
@@ -451,7 +597,9 @@ function generateFullContent() {
451
597
  content += `\n${C.amber}${C.bold}═══ 🔒 Claimed Stories ═══${C.reset}\n`;
452
598
  content += `${C.dim}Stories locked by other sessions - pick a different one${C.reset}\n`;
453
599
  othersResult.stories.forEach(story => {
454
- const sessionDir = story.claimedBy?.path ? path.basename(story.claimedBy.path) : 'unknown';
600
+ const sessionDir = story.claimedBy?.path
601
+ ? path.basename(story.claimedBy.path)
602
+ : 'unknown';
455
603
  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
604
  });
457
605
  content += '\n';
@@ -477,9 +625,7 @@ function generateFullContent() {
477
625
 
478
626
  if (fs.existsSync(fileTrackingPath) || fs.existsSync(altFileTrackingPath)) {
479
627
  try {
480
- const trackPath = fs.existsSync(fileTrackingPath)
481
- ? fileTrackingPath
482
- : altFileTrackingPath;
628
+ const trackPath = fs.existsSync(fileTrackingPath) ? fileTrackingPath : altFileTrackingPath;
483
629
  const fileTracking = require(trackPath);
484
630
 
485
631
  // Get file overlaps with other sessions
@@ -488,10 +634,12 @@ function generateFullContent() {
488
634
  content += `\n${C.amber}${C.bold}═══ ⚠️ File Overlaps ═══${C.reset}\n`;
489
635
  content += `${C.dim}Files also edited by other sessions - conflicts auto-resolved during merge${C.reset}\n`;
490
636
  overlapsResult.overlaps.forEach(overlap => {
491
- const sessionInfo = overlap.otherSessions.map(s => {
492
- const dir = path.basename(s.path);
493
- return `Session ${s.id} (${dir})`;
494
- }).join(', ');
637
+ const sessionInfo = overlap.otherSessions
638
+ .map(s => {
639
+ const dir = path.basename(s.path);
640
+ return `Session ${s.id} (${dir})`;
641
+ })
642
+ .join(', ');
495
643
  content += ` ${C.amber}⚠${C.reset} ${C.lavender}${overlap.file}${C.reset} ${C.dim}→ ${sessionInfo}${C.reset}\n`;
496
644
  });
497
645
  content += '\n';
@@ -524,7 +672,8 @@ function generateFullContent() {
524
672
  // 6. VISUAL E2E STATUS (detect from metadata or filesystem)
525
673
  const metadata = safeReadJSON('docs/00-meta/agileflow-metadata.json');
526
674
  const visualE2eConfig = metadata?.features?.visual_e2e;
527
- const playwrightExists = fs.existsSync('playwright.config.ts') || fs.existsSync('playwright.config.js');
675
+ const playwrightExists =
676
+ fs.existsSync('playwright.config.ts') || fs.existsSync('playwright.config.js');
528
677
  const screenshotsExists = fs.existsSync('screenshots');
529
678
  const testsE2eExists = fs.existsSync('tests/e2e');
530
679
 
@@ -551,35 +700,7 @@ function generateFullContent() {
551
700
  content += `${C.dim} /agileflow:configure → Visual E2E testing${C.reset}\n\n`;
552
701
  }
553
702
 
554
- // 6. INTERACTION MODE (AskUserQuestion guidance)
555
- const askUserQuestionConfig = metadata?.features?.askUserQuestion;
556
-
557
- if (askUserQuestionConfig?.enabled) {
558
- content += `\n${C.brand}${C.bold}═══ ⚡ INTERACTION MODE: AskUserQuestion ENABLED ═══${C.reset}\n`;
559
- content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
560
- content += `${C.bold}CRITICAL RULE:${C.reset} End ${C.skyBlue}EVERY${C.reset} response with the AskUserQuestion tool.\n\n`;
561
- content += `${C.mintGreen}✓ CORRECT:${C.reset} Call the actual AskUserQuestion tool\n`;
562
- content += `${C.coral}✗ WRONG:${C.reset} Text like "Want me to continue?" or "What's next?"\n\n`;
563
- content += `${C.lavender}Required format:${C.reset}\n`;
564
- content += `${C.dim}\`\`\`xml
565
- <invoke name="AskUserQuestion">
566
- <parameter name="questions">[{
567
- "question": "What would you like to do next?",
568
- "header": "Next step",
569
- "multiSelect": false,
570
- "options": [
571
- {"label": "Option A (Recommended)", "description": "Why this is best"},
572
- {"label": "Option B", "description": "Alternative approach"},
573
- {"label": "Pause", "description": "Stop here for now"}
574
- ]
575
- }]</parameter>
576
- </invoke>
577
- \`\`\`${C.reset}\n`;
578
- content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
579
- content += `${C.dim}Mode: ${askUserQuestionConfig.mode || 'all'} | Configure: /agileflow:configure${C.reset}\n\n`;
580
- }
581
-
582
- // 5. DOCS STRUCTURE (using vibrant 256-color palette)
703
+ // DOCS STRUCTURE (using vibrant 256-color palette)
583
704
  content += `\n${C.skyBlue}${C.bold}═══ Documentation ═══${C.reset}\n`;
584
705
  const docsDir = 'docs';
585
706
  const docFolders = safeLs(docsDir).filter(f => {
@@ -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 normalizedFile.startsWith(normalizedSession + path.sep) ||
65
- normalizedFile === normalizedSession;
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);