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
|
@@ -21,10 +21,72 @@ const { execSync } = require('child_process');
|
|
|
21
21
|
const { c: C, box } = require('../lib/colors');
|
|
22
22
|
const { isValidCommandName } = require('../lib/validate');
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
// Claude Code's Bash tool truncates around 30K chars, but ANSI codes and
|
|
25
|
+
// box-drawing characters (╭╮╰╯─│) are multi-byte UTF-8, so we need buffer.
|
|
26
|
+
// Summary table should be the LAST thing visible before truncation.
|
|
27
|
+
const DISPLAY_LIMIT = 29200;
|
|
25
28
|
|
|
26
|
-
//
|
|
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
|
|
27
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
|
|
28
90
|
if (commandName && isValidCommandName(commandName)) {
|
|
29
91
|
const sessionStatePath = 'docs/09-agents/session-state.json';
|
|
30
92
|
if (fs.existsSync(sessionStatePath)) {
|
|
@@ -39,11 +101,13 @@ if (commandName && isValidCommandName(commandName)) {
|
|
|
39
101
|
// Remove any existing entry for this command (avoid duplicates)
|
|
40
102
|
state.active_commands = state.active_commands.filter(c => c.name !== commandName);
|
|
41
103
|
|
|
42
|
-
// Add the new command
|
|
104
|
+
// Add the new command with active sections for progressive disclosure
|
|
43
105
|
state.active_commands.push({
|
|
44
106
|
name: commandName,
|
|
45
107
|
activated_at: new Date().toISOString(),
|
|
46
108
|
state: {},
|
|
109
|
+
active_sections: activeSections,
|
|
110
|
+
params: commandParams,
|
|
47
111
|
});
|
|
48
112
|
|
|
49
113
|
// Remove legacy active_command field (only use active_commands array now)
|
|
@@ -259,6 +323,13 @@ function generateSummary() {
|
|
|
259
323
|
summary += row('⭐ Up Next', readyStories.slice(0, 3).join(', '), C.skyBlue, C.skyBlue);
|
|
260
324
|
}
|
|
261
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
|
+
|
|
262
333
|
summary += divider();
|
|
263
334
|
|
|
264
335
|
// Key files (using vibrant 256-color palette)
|
|
@@ -300,8 +371,6 @@ function generateSummary() {
|
|
|
300
371
|
);
|
|
301
372
|
|
|
302
373
|
summary += bottomBorder;
|
|
303
|
-
summary += '\n';
|
|
304
|
-
summary += `${C.dim}Full context continues below (Claude sees all)...${C.reset}\n\n`;
|
|
305
374
|
|
|
306
375
|
return summary;
|
|
307
376
|
}
|
|
@@ -317,6 +386,35 @@ function generateFullContent() {
|
|
|
317
386
|
content += `${C.lavender}${C.bold}${title}${C.reset}\n`;
|
|
318
387
|
content += `${C.dim}Generated: ${new Date().toISOString()}${C.reset}\n`;
|
|
319
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
|
+
|
|
320
418
|
// 1. GIT STATUS (using vibrant 256-color palette)
|
|
321
419
|
content += `\n${C.skyBlue}${C.bold}═══ Git Status ═══${C.reset}\n`;
|
|
322
420
|
const branch = safeExec('git branch --show-current') || 'unknown';
|
|
@@ -375,6 +473,19 @@ function generateFullContent() {
|
|
|
375
473
|
// Backwards compatibility for old format
|
|
376
474
|
content += `Active command: ${C.skyBlue}${sessionState.active_command.name}${C.reset}\n`;
|
|
377
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
|
+
}
|
|
378
489
|
} else {
|
|
379
490
|
content += `${C.dim}No session-state.json found${C.reset}\n`;
|
|
380
491
|
}
|
|
@@ -439,9 +550,7 @@ function generateFullContent() {
|
|
|
439
550
|
|
|
440
551
|
if (fs.existsSync(storyClaimingPath) || fs.existsSync(altStoryClaimingPath)) {
|
|
441
552
|
try {
|
|
442
|
-
const claimPath = fs.existsSync(storyClaimingPath)
|
|
443
|
-
? storyClaimingPath
|
|
444
|
-
: altStoryClaimingPath;
|
|
553
|
+
const claimPath = fs.existsSync(storyClaimingPath) ? storyClaimingPath : altStoryClaimingPath;
|
|
445
554
|
const storyClaiming = require(claimPath);
|
|
446
555
|
|
|
447
556
|
// Get stories claimed by other sessions
|
|
@@ -450,7 +559,9 @@ function generateFullContent() {
|
|
|
450
559
|
content += `\n${C.amber}${C.bold}═══ 🔒 Claimed Stories ═══${C.reset}\n`;
|
|
451
560
|
content += `${C.dim}Stories locked by other sessions - pick a different one${C.reset}\n`;
|
|
452
561
|
othersResult.stories.forEach(story => {
|
|
453
|
-
const sessionDir = story.claimedBy?.path
|
|
562
|
+
const sessionDir = story.claimedBy?.path
|
|
563
|
+
? path.basename(story.claimedBy.path)
|
|
564
|
+
: 'unknown';
|
|
454
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`;
|
|
455
566
|
});
|
|
456
567
|
content += '\n';
|
|
@@ -476,9 +587,7 @@ function generateFullContent() {
|
|
|
476
587
|
|
|
477
588
|
if (fs.existsSync(fileTrackingPath) || fs.existsSync(altFileTrackingPath)) {
|
|
478
589
|
try {
|
|
479
|
-
const trackPath = fs.existsSync(fileTrackingPath)
|
|
480
|
-
? fileTrackingPath
|
|
481
|
-
: altFileTrackingPath;
|
|
590
|
+
const trackPath = fs.existsSync(fileTrackingPath) ? fileTrackingPath : altFileTrackingPath;
|
|
482
591
|
const fileTracking = require(trackPath);
|
|
483
592
|
|
|
484
593
|
// Get file overlaps with other sessions
|
|
@@ -487,10 +596,12 @@ function generateFullContent() {
|
|
|
487
596
|
content += `\n${C.amber}${C.bold}═══ ⚠️ File Overlaps ═══${C.reset}\n`;
|
|
488
597
|
content += `${C.dim}Files also edited by other sessions - conflicts auto-resolved during merge${C.reset}\n`;
|
|
489
598
|
overlapsResult.overlaps.forEach(overlap => {
|
|
490
|
-
const sessionInfo = overlap.otherSessions
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
599
|
+
const sessionInfo = overlap.otherSessions
|
|
600
|
+
.map(s => {
|
|
601
|
+
const dir = path.basename(s.path);
|
|
602
|
+
return `Session ${s.id} (${dir})`;
|
|
603
|
+
})
|
|
604
|
+
.join(', ');
|
|
494
605
|
content += ` ${C.amber}⚠${C.reset} ${C.lavender}${overlap.file}${C.reset} ${C.dim}→ ${sessionInfo}${C.reset}\n`;
|
|
495
606
|
});
|
|
496
607
|
content += '\n';
|
|
@@ -523,7 +634,8 @@ function generateFullContent() {
|
|
|
523
634
|
// 6. VISUAL E2E STATUS (detect from metadata or filesystem)
|
|
524
635
|
const metadata = safeReadJSON('docs/00-meta/agileflow-metadata.json');
|
|
525
636
|
const visualE2eConfig = metadata?.features?.visual_e2e;
|
|
526
|
-
const playwrightExists =
|
|
637
|
+
const playwrightExists =
|
|
638
|
+
fs.existsSync('playwright.config.ts') || fs.existsSync('playwright.config.js');
|
|
527
639
|
const screenshotsExists = fs.existsSync('screenshots');
|
|
528
640
|
const testsE2eExists = fs.existsSync('tests/e2e');
|
|
529
641
|
|
|
@@ -706,11 +818,11 @@ if (fullContent.length <= cutoffPoint) {
|
|
|
706
818
|
console.log(fullContent);
|
|
707
819
|
console.log(summary);
|
|
708
820
|
} else {
|
|
709
|
-
//
|
|
821
|
+
// Output content up to cutoff, then summary as the LAST visible thing.
|
|
822
|
+
// Don't output contentAfter - it would bleed into visible area before truncation,
|
|
823
|
+
// and Claude only sees ~30K chars from Bash anyway.
|
|
710
824
|
const contentBefore = fullContent.substring(0, cutoffPoint);
|
|
711
|
-
const contentAfter = fullContent.substring(cutoffPoint);
|
|
712
825
|
|
|
713
826
|
console.log(contentBefore);
|
|
714
827
|
console.log(summary);
|
|
715
|
-
console.log(contentAfter);
|
|
716
828
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* session-boundary.js - PreToolUse hook for Edit/Write session isolation
|
|
4
|
+
*
|
|
5
|
+
* Prevents Claude from editing files outside the active session directory.
|
|
6
|
+
* Used with PreToolUse:Edit and PreToolUse:Write hooks.
|
|
7
|
+
*
|
|
8
|
+
* Exit codes:
|
|
9
|
+
* 0 = Allow the operation
|
|
10
|
+
* 2 = Block with message (shown to Claude)
|
|
11
|
+
*
|
|
12
|
+
* Input: JSON on stdin with tool_input.file_path
|
|
13
|
+
* Output: Error message to stderr if blocking
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
// Inline colors (no external dependency)
|
|
20
|
+
const c = {
|
|
21
|
+
coral: '\x1b[38;5;203m',
|
|
22
|
+
dim: '\x1b[2m',
|
|
23
|
+
reset: '\x1b[0m',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const STDIN_TIMEOUT_MS = 4000;
|
|
27
|
+
|
|
28
|
+
// Find project root by looking for .agileflow directory
|
|
29
|
+
function findProjectRoot() {
|
|
30
|
+
let dir = process.cwd();
|
|
31
|
+
while (dir !== '/') {
|
|
32
|
+
if (fs.existsSync(path.join(dir, '.agileflow'))) {
|
|
33
|
+
return dir;
|
|
34
|
+
}
|
|
35
|
+
if (fs.existsSync(path.join(dir, 'docs', '09-agents'))) {
|
|
36
|
+
return dir;
|
|
37
|
+
}
|
|
38
|
+
dir = path.dirname(dir);
|
|
39
|
+
}
|
|
40
|
+
return process.cwd();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ROOT = findProjectRoot();
|
|
44
|
+
const SESSION_STATE_PATH = path.join(ROOT, 'docs', '09-agents', 'session-state.json');
|
|
45
|
+
|
|
46
|
+
// Get active session from session-state.json
|
|
47
|
+
function getActiveSession() {
|
|
48
|
+
try {
|
|
49
|
+
if (!fs.existsSync(SESSION_STATE_PATH)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const data = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
|
|
53
|
+
return data.active_session || null;
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check if filePath is inside sessionPath
|
|
60
|
+
function isInsideSession(filePath, sessionPath) {
|
|
61
|
+
const normalizedFile = path.resolve(filePath);
|
|
62
|
+
const normalizedSession = path.resolve(sessionPath);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
normalizedFile.startsWith(normalizedSession + path.sep) || normalizedFile === normalizedSession
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Output blocked message
|
|
70
|
+
function outputBlocked(filePath, activeSession) {
|
|
71
|
+
const sessionName = activeSession.nickname
|
|
72
|
+
? `"${activeSession.nickname}"`
|
|
73
|
+
: `Session ${activeSession.id}`;
|
|
74
|
+
|
|
75
|
+
console.error(`${c.coral}[SESSION BOUNDARY]${c.reset} Edit blocked`);
|
|
76
|
+
console.error(`${c.dim}File: ${filePath}${c.reset}`);
|
|
77
|
+
console.error(`${c.dim}Active session: ${sessionName} (${activeSession.path})${c.reset}`);
|
|
78
|
+
console.error('');
|
|
79
|
+
console.error(`${c.dim}The file is outside the active session directory.${c.reset}`);
|
|
80
|
+
console.error(`${c.dim}Use /agileflow:session:resume to switch sessions first.${c.reset}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Main logic - run with stdin events (async)
|
|
84
|
+
function main() {
|
|
85
|
+
let inputData = '';
|
|
86
|
+
|
|
87
|
+
process.stdin.setEncoding('utf8');
|
|
88
|
+
|
|
89
|
+
process.stdin.on('data', chunk => {
|
|
90
|
+
inputData += chunk;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
process.stdin.on('end', () => {
|
|
94
|
+
try {
|
|
95
|
+
// Parse tool input from Claude Code
|
|
96
|
+
const hookData = JSON.parse(inputData);
|
|
97
|
+
const filePath = hookData?.tool_input?.file_path;
|
|
98
|
+
|
|
99
|
+
if (!filePath) {
|
|
100
|
+
// No file path in input - allow
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Get active session
|
|
105
|
+
const activeSession = getActiveSession();
|
|
106
|
+
|
|
107
|
+
if (!activeSession || !activeSession.path) {
|
|
108
|
+
// No active session set - allow all (normal behavior)
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check if file is inside active session
|
|
113
|
+
if (isInsideSession(filePath, activeSession.path)) {
|
|
114
|
+
// File is inside active session - allow
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// File is OUTSIDE active session - BLOCK
|
|
119
|
+
outputBlocked(filePath, activeSession);
|
|
120
|
+
process.exit(2);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
// Parse error or other issue - fail open
|
|
123
|
+
process.exit(0);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Handle no stdin (direct invocation)
|
|
128
|
+
process.stdin.on('error', () => {
|
|
129
|
+
process.exit(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Set timeout to prevent hanging
|
|
133
|
+
setTimeout(() => {
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}, STDIN_TIMEOUT_MS);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
main();
|