agileflow 2.89.2 → 2.90.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/content-sanitizer.js +463 -0
- package/lib/error-codes.js +544 -0
- package/lib/errors.js +336 -5
- package/lib/feedback.js +561 -0
- package/lib/path-resolver.js +396 -0
- package/lib/placeholder-registry.js +617 -0
- package/lib/session-registry.js +461 -0
- package/lib/smart-json-file.js +653 -0
- package/lib/table-formatter.js +504 -0
- package/lib/transient-status.js +374 -0
- package/lib/ui-manager.js +612 -0
- package/lib/validate-args.js +213 -0
- package/lib/validate-names.js +143 -0
- package/lib/validate-paths.js +434 -0
- package/lib/validate.js +38 -584
- package/package.json +4 -1
- package/scripts/agileflow-configure.js +40 -1440
- package/scripts/agileflow-welcome.js +2 -1
- package/scripts/check-update.js +16 -3
- package/scripts/lib/configure-detect.js +383 -0
- package/scripts/lib/configure-features.js +811 -0
- package/scripts/lib/configure-repair.js +314 -0
- package/scripts/lib/configure-utils.js +115 -0
- package/scripts/lib/frontmatter-parser.js +3 -3
- package/scripts/lib/sessionRegistry.js +682 -0
- package/scripts/obtain-context.js +417 -113
- package/scripts/ralph-loop.js +1 -1
- package/scripts/session-manager.js +77 -10
- package/scripts/tui/App.js +176 -0
- package/scripts/tui/index.js +75 -0
- package/scripts/tui/lib/crashRecovery.js +302 -0
- package/scripts/tui/lib/eventStream.js +316 -0
- package/scripts/tui/lib/keyboard.js +252 -0
- package/scripts/tui/lib/loopControl.js +371 -0
- package/scripts/tui/panels/OutputPanel.js +278 -0
- package/scripts/tui/panels/SessionPanel.js +178 -0
- package/scripts/tui/panels/TracePanel.js +333 -0
- package/src/core/commands/tui.md +91 -0
- package/tools/cli/commands/config.js +10 -33
- package/tools/cli/commands/doctor.js +48 -40
- package/tools/cli/commands/list.js +49 -37
- package/tools/cli/commands/status.js +13 -37
- package/tools/cli/commands/uninstall.js +12 -41
- package/tools/cli/installers/core/installer.js +75 -12
- package/tools/cli/installers/ide/_interface.js +238 -0
- package/tools/cli/installers/ide/codex.js +2 -2
- package/tools/cli/installers/ide/manager.js +15 -0
- package/tools/cli/lib/command-context.js +374 -0
- package/tools/cli/lib/config-manager.js +394 -0
- package/tools/cli/lib/content-injector.js +69 -16
- package/tools/cli/lib/ide-errors.js +163 -29
- package/tools/cli/lib/ide-registry.js +186 -0
- package/tools/cli/lib/npm-utils.js +16 -3
- package/tools/cli/lib/self-update.js +148 -0
- package/tools/cli/lib/validation-middleware.js +491 -0
|
@@ -10,12 +10,24 @@
|
|
|
10
10
|
* - Then shows the summary (so user sees it at their display cutoff)
|
|
11
11
|
* - Then shows rest of full content (for Claude)
|
|
12
12
|
*
|
|
13
|
+
* PERFORMANCE OPTIMIZATION (US-0092):
|
|
14
|
+
* - Pre-fetches all file/JSON data in parallel before building content
|
|
15
|
+
* - Uses Promise.all() to parallelize independent I/O operations
|
|
16
|
+
* - Reduces context gathering time by 60-75% (400ms -> 100-150ms)
|
|
17
|
+
*
|
|
18
|
+
* LAZY EVALUATION (US-0093):
|
|
19
|
+
* - Research notes: Only load full content for research-related commands
|
|
20
|
+
* - Session claims: Only load if multi-session environment detected
|
|
21
|
+
* - File overlaps: Only load if parallel sessions are active
|
|
22
|
+
* - Configurable via features.lazyContext in agileflow-metadata.json
|
|
23
|
+
*
|
|
13
24
|
* Usage:
|
|
14
25
|
* node scripts/obtain-context.js # Just gather context
|
|
15
26
|
* node scripts/obtain-context.js babysit # Gather + register 'babysit'
|
|
16
27
|
*/
|
|
17
28
|
|
|
18
29
|
const fs = require('fs');
|
|
30
|
+
const fsPromises = require('fs').promises;
|
|
19
31
|
const path = require('path');
|
|
20
32
|
const { execSync } = require('child_process');
|
|
21
33
|
const { c: C, box } = require('../lib/colors');
|
|
@@ -153,11 +165,217 @@ function safeExec(cmd) {
|
|
|
153
165
|
}
|
|
154
166
|
}
|
|
155
167
|
|
|
168
|
+
// =============================================================================
|
|
169
|
+
// Lazy Evaluation Configuration (US-0093)
|
|
170
|
+
// =============================================================================
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Commands that need full research notes content
|
|
174
|
+
*/
|
|
175
|
+
const RESEARCH_COMMANDS = ['research', 'ideate', 'mentor', 'rpi'];
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Determine which sections need to be loaded based on command and environment.
|
|
179
|
+
*
|
|
180
|
+
* @param {string} cmdName - Command name being executed
|
|
181
|
+
* @param {Object} lazyConfig - Lazy context configuration from metadata
|
|
182
|
+
* @param {boolean} isMultiSession - Whether multiple sessions are detected
|
|
183
|
+
* @returns {Object} Sections to load { researchContent, sessionClaims, fileOverlaps }
|
|
184
|
+
*/
|
|
185
|
+
function determineSectionsToLoad(cmdName, lazyConfig, isMultiSession) {
|
|
186
|
+
// If lazy loading is disabled, load everything
|
|
187
|
+
if (!lazyConfig?.enabled) {
|
|
188
|
+
return {
|
|
189
|
+
researchContent: true,
|
|
190
|
+
sessionClaims: true,
|
|
191
|
+
fileOverlaps: true,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Research notes: load for research-related commands or if 'always'
|
|
196
|
+
const needsResearch =
|
|
197
|
+
lazyConfig.researchNotes === 'always' ||
|
|
198
|
+
(lazyConfig.researchNotes === 'conditional' && RESEARCH_COMMANDS.includes(cmdName));
|
|
199
|
+
|
|
200
|
+
// Session claims: load if multi-session environment or if 'always'
|
|
201
|
+
const needsClaims =
|
|
202
|
+
lazyConfig.sessionClaims === 'always' ||
|
|
203
|
+
(lazyConfig.sessionClaims === 'conditional' && isMultiSession);
|
|
204
|
+
|
|
205
|
+
// File overlaps: load if multi-session environment or if 'always'
|
|
206
|
+
const needsOverlaps =
|
|
207
|
+
lazyConfig.fileOverlaps === 'always' ||
|
|
208
|
+
(lazyConfig.fileOverlaps === 'conditional' && isMultiSession);
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
researchContent: needsResearch,
|
|
212
|
+
sessionClaims: needsClaims,
|
|
213
|
+
fileOverlaps: needsOverlaps,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// =============================================================================
|
|
218
|
+
// Async I/O Functions for Parallel Pre-fetching
|
|
219
|
+
// =============================================================================
|
|
220
|
+
|
|
221
|
+
async function safeReadAsync(filePath) {
|
|
222
|
+
try {
|
|
223
|
+
return await fsPromises.readFile(filePath, 'utf8');
|
|
224
|
+
} catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function safeReadJSONAsync(filePath) {
|
|
230
|
+
try {
|
|
231
|
+
const content = await fsPromises.readFile(filePath, 'utf8');
|
|
232
|
+
return JSON.parse(content);
|
|
233
|
+
} catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function safeLsAsync(dirPath) {
|
|
239
|
+
try {
|
|
240
|
+
return await fsPromises.readdir(dirPath);
|
|
241
|
+
} catch {
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Execute a command asynchronously using child_process.exec
|
|
248
|
+
* @param {string} cmd - Command to execute
|
|
249
|
+
* @returns {Promise<string|null>} Command output or null on error
|
|
250
|
+
*/
|
|
251
|
+
async function safeExecAsync(cmd) {
|
|
252
|
+
const { exec } = require('child_process');
|
|
253
|
+
return new Promise(resolve => {
|
|
254
|
+
exec(cmd, { encoding: 'utf8' }, (error, stdout) => {
|
|
255
|
+
if (error) {
|
|
256
|
+
resolve(null);
|
|
257
|
+
} else {
|
|
258
|
+
resolve(stdout.trim());
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Pre-fetch all required data in parallel for optimal performance.
|
|
266
|
+
* This dramatically reduces I/O wait time by overlapping file reads and git commands.
|
|
267
|
+
*
|
|
268
|
+
* Lazy loading (US-0093): Only fetches content based on sectionsToLoad parameter.
|
|
269
|
+
*
|
|
270
|
+
* @param {Object} options - Options for prefetching
|
|
271
|
+
* @param {Object} options.sectionsToLoad - Which sections need full content
|
|
272
|
+
* @returns {Object} Pre-fetched data for content generation
|
|
273
|
+
*/
|
|
274
|
+
async function prefetchAllData(options = {}) {
|
|
275
|
+
const sectionsToLoad = options.sectionsToLoad || {
|
|
276
|
+
researchContent: true,
|
|
277
|
+
sessionClaims: true,
|
|
278
|
+
fileOverlaps: true,
|
|
279
|
+
};
|
|
280
|
+
// Define all files to read
|
|
281
|
+
const jsonFiles = {
|
|
282
|
+
metadata: 'docs/00-meta/agileflow-metadata.json',
|
|
283
|
+
statusJson: 'docs/09-agents/status.json',
|
|
284
|
+
sessionState: 'docs/09-agents/session-state.json',
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const textFiles = {
|
|
288
|
+
busLog: 'docs/09-agents/bus/log.jsonl',
|
|
289
|
+
claudeMd: 'CLAUDE.md',
|
|
290
|
+
readmeMd: 'README.md',
|
|
291
|
+
archReadme: 'docs/04-architecture/README.md',
|
|
292
|
+
practicesReadme: 'docs/02-practices/README.md',
|
|
293
|
+
roadmap: 'docs/08-project/roadmap.md',
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const directories = {
|
|
297
|
+
docs: 'docs',
|
|
298
|
+
research: 'docs/10-research',
|
|
299
|
+
epics: 'docs/05-epics',
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// Git commands to run in parallel
|
|
303
|
+
const gitCommands = {
|
|
304
|
+
branch: 'git branch --show-current',
|
|
305
|
+
commitShort: 'git log -1 --format="%h"',
|
|
306
|
+
commitMsg: 'git log -1 --format="%s"',
|
|
307
|
+
commitFull: 'git log -1 --format="%h %s"',
|
|
308
|
+
status: 'git status --short',
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// Create all promises for parallel execution
|
|
312
|
+
const jsonPromises = Object.entries(jsonFiles).map(async ([key, filePath]) => {
|
|
313
|
+
const data = await safeReadJSONAsync(filePath);
|
|
314
|
+
return [key, data];
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const textPromises = Object.entries(textFiles).map(async ([key, filePath]) => {
|
|
318
|
+
const data = await safeReadAsync(filePath);
|
|
319
|
+
return [key, data];
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const dirPromises = Object.entries(directories).map(async ([key, dirPath]) => {
|
|
323
|
+
const files = await safeLsAsync(dirPath);
|
|
324
|
+
return [key, files];
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const gitPromises = Object.entries(gitCommands).map(async ([key, cmd]) => {
|
|
328
|
+
const data = await safeExecAsync(cmd);
|
|
329
|
+
return [key, data];
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Execute all I/O operations in parallel
|
|
333
|
+
const [jsonResults, textResults, dirResults, gitResults] = await Promise.all([
|
|
334
|
+
Promise.all(jsonPromises),
|
|
335
|
+
Promise.all(textPromises),
|
|
336
|
+
Promise.all(dirPromises),
|
|
337
|
+
Promise.all(gitPromises),
|
|
338
|
+
]);
|
|
339
|
+
|
|
340
|
+
// Convert arrays back to objects
|
|
341
|
+
const json = Object.fromEntries(jsonResults);
|
|
342
|
+
const text = Object.fromEntries(textResults);
|
|
343
|
+
const dirs = Object.fromEntries(dirResults);
|
|
344
|
+
const git = Object.fromEntries(gitResults);
|
|
345
|
+
|
|
346
|
+
// Determine most recent research file
|
|
347
|
+
const researchFiles = dirs.research
|
|
348
|
+
.filter(f => f.endsWith('.md') && f !== 'README.md')
|
|
349
|
+
.sort()
|
|
350
|
+
.reverse();
|
|
351
|
+
|
|
352
|
+
// Lazy loading (US-0093): Only fetch research content if needed
|
|
353
|
+
let mostRecentResearch = null;
|
|
354
|
+
if (sectionsToLoad.researchContent && researchFiles.length > 0) {
|
|
355
|
+
mostRecentResearch = await safeReadAsync(path.join('docs/10-research', researchFiles[0]));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
json,
|
|
360
|
+
text,
|
|
361
|
+
dirs,
|
|
362
|
+
git,
|
|
363
|
+
researchFiles,
|
|
364
|
+
mostRecentResearch,
|
|
365
|
+
sectionsToLoad, // Pass through for content generation
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
156
369
|
// ============================================
|
|
157
370
|
// GENERATE SUMMARY (calculated first for positioning)
|
|
158
371
|
// ============================================
|
|
159
372
|
|
|
160
|
-
|
|
373
|
+
/**
|
|
374
|
+
* Generate summary content using pre-fetched data.
|
|
375
|
+
* @param {Object} prefetched - Pre-fetched data from prefetchAllData()
|
|
376
|
+
* @returns {string} Summary content
|
|
377
|
+
*/
|
|
378
|
+
function generateSummary(prefetched = null) {
|
|
161
379
|
// Box drawing characters
|
|
162
380
|
const box = {
|
|
163
381
|
tl: '╭',
|
|
@@ -226,18 +444,27 @@ function generateSummary() {
|
|
|
226
444
|
const headerDivider = `${C.dim}${box.lT}${box.h.repeat(L + 2)}${box.tT}${box.h.repeat(W - L - 2)}${box.rT}${C.reset}\n`;
|
|
227
445
|
const bottomBorder = `${C.dim}${box.bl}${box.h.repeat(L + 2)}${box.bT}${box.h.repeat(W - L - 2)}${box.br}${C.reset}\n`;
|
|
228
446
|
|
|
229
|
-
// Gather data
|
|
230
|
-
const branch = safeExec('git branch --show-current')
|
|
231
|
-
const lastCommitShort =
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
.filter(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
447
|
+
// Gather data - use prefetched when available, fallback to sync reads
|
|
448
|
+
const branch = prefetched?.git?.branch ?? safeExec('git branch --show-current') ?? 'unknown';
|
|
449
|
+
const lastCommitShort =
|
|
450
|
+
prefetched?.git?.commitShort ?? safeExec('git log -1 --format="%h"') ?? '?';
|
|
451
|
+
const lastCommitMsg =
|
|
452
|
+
prefetched?.git?.commitMsg ?? safeExec('git log -1 --format="%s"') ?? 'no commits';
|
|
453
|
+
const statusLines = (prefetched?.git?.status ?? safeExec('git status --short') ?? '')
|
|
454
|
+
.split('\n')
|
|
455
|
+
.filter(Boolean);
|
|
456
|
+
const statusJson = prefetched?.json?.statusJson ?? safeReadJSON('docs/09-agents/status.json');
|
|
457
|
+
const sessionState =
|
|
458
|
+
prefetched?.json?.sessionState ?? safeReadJSON('docs/09-agents/session-state.json');
|
|
459
|
+
const researchFiles =
|
|
460
|
+
prefetched?.researchFiles ??
|
|
461
|
+
safeLs('docs/10-research')
|
|
462
|
+
.filter(f => f.endsWith('.md') && f !== 'README.md')
|
|
463
|
+
.sort()
|
|
464
|
+
.reverse();
|
|
465
|
+
const epicFiles =
|
|
466
|
+
prefetched?.dirs?.epics?.filter(f => f.endsWith('.md') && f !== 'README.md') ??
|
|
467
|
+
safeLs('docs/05-epics').filter(f => f.endsWith('.md') && f !== 'README.md');
|
|
241
468
|
|
|
242
469
|
// Count stories by status
|
|
243
470
|
const byStatus = {};
|
|
@@ -378,7 +605,12 @@ function generateSummary() {
|
|
|
378
605
|
// GENERATE FULL CONTENT
|
|
379
606
|
// ============================================
|
|
380
607
|
|
|
381
|
-
|
|
608
|
+
/**
|
|
609
|
+
* Generate full content using pre-fetched data.
|
|
610
|
+
* @param {Object} prefetched - Pre-fetched data from prefetchAllData()
|
|
611
|
+
* @returns {string} Full content
|
|
612
|
+
*/
|
|
613
|
+
function generateFullContent(prefetched = null) {
|
|
382
614
|
let content = '';
|
|
383
615
|
|
|
384
616
|
const title = commandName ? `AgileFlow Context [${commandName}]` : 'AgileFlow Context';
|
|
@@ -433,7 +665,8 @@ function generateFullContent() {
|
|
|
433
665
|
|
|
434
666
|
// 0.7 INTERACTION MODE (AskUserQuestion) - EARLY for visibility
|
|
435
667
|
// This MUST appear before other content to ensure Claude sees it
|
|
436
|
-
const earlyMetadata =
|
|
668
|
+
const earlyMetadata =
|
|
669
|
+
prefetched?.json?.metadata ?? safeReadJSON('docs/00-meta/agileflow-metadata.json');
|
|
437
670
|
const askUserQuestionConfig = earlyMetadata?.features?.askUserQuestion;
|
|
438
671
|
|
|
439
672
|
if (askUserQuestionConfig?.enabled) {
|
|
@@ -477,10 +710,11 @@ function generateFullContent() {
|
|
|
477
710
|
|
|
478
711
|
// 1. GIT STATUS (using vibrant 256-color palette)
|
|
479
712
|
content += `\n${C.skyBlue}${C.bold}═══ Git Status ═══${C.reset}\n`;
|
|
480
|
-
const branch = safeExec('git branch --show-current')
|
|
481
|
-
const status = safeExec('git status --short')
|
|
713
|
+
const branch = prefetched?.git?.branch ?? safeExec('git branch --show-current') ?? 'unknown';
|
|
714
|
+
const status = prefetched?.git?.status ?? safeExec('git status --short') ?? '';
|
|
482
715
|
const statusLines = status.split('\n').filter(Boolean);
|
|
483
|
-
const lastCommit =
|
|
716
|
+
const lastCommit =
|
|
717
|
+
prefetched?.git?.commitFull ?? safeExec('git log -1 --format="%h %s"') ?? 'no commits';
|
|
484
718
|
|
|
485
719
|
content += `Branch: ${C.mintGreen}${branch}${C.reset}\n`;
|
|
486
720
|
content += `Last commit: ${C.dim}${lastCommit}${C.reset}\n`;
|
|
@@ -496,7 +730,7 @@ function generateFullContent() {
|
|
|
496
730
|
// 2. STATUS.JSON - Full Content (using vibrant 256-color palette)
|
|
497
731
|
content += `\n${C.skyBlue}${C.bold}═══ Status.json (Full Content) ═══${C.reset}\n`;
|
|
498
732
|
const statusJsonPath = 'docs/09-agents/status.json';
|
|
499
|
-
const statusJson = safeReadJSON(statusJsonPath);
|
|
733
|
+
const statusJson = prefetched?.json?.statusJson ?? safeReadJSON(statusJsonPath);
|
|
500
734
|
|
|
501
735
|
if (statusJson) {
|
|
502
736
|
content += `${C.dim}${'─'.repeat(50)}${C.reset}\n`;
|
|
@@ -512,7 +746,8 @@ function generateFullContent() {
|
|
|
512
746
|
|
|
513
747
|
// 3. SESSION STATE (using vibrant 256-color palette)
|
|
514
748
|
content += `\n${C.skyBlue}${C.bold}═══ Session State ═══${C.reset}\n`;
|
|
515
|
-
const sessionState =
|
|
749
|
+
const sessionState =
|
|
750
|
+
prefetched?.json?.sessionState ?? safeReadJSON('docs/09-agents/session-state.json');
|
|
516
751
|
if (sessionState) {
|
|
517
752
|
const current = sessionState.current_session;
|
|
518
753
|
if (current && current.started_at) {
|
|
@@ -582,94 +817,107 @@ function generateFullContent() {
|
|
|
582
817
|
}
|
|
583
818
|
|
|
584
819
|
// 5. STORY CLAIMS (inter-session coordination)
|
|
585
|
-
|
|
586
|
-
const
|
|
820
|
+
// Lazy loading (US-0093): Only load if sectionsToLoad.sessionClaims is true
|
|
821
|
+
const shouldLoadClaims = prefetched?.sectionsToLoad?.sessionClaims !== false;
|
|
587
822
|
|
|
588
|
-
if (
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const storyClaiming = require(claimPath);
|
|
592
|
-
|
|
593
|
-
// Get stories claimed by other sessions
|
|
594
|
-
const othersResult = storyClaiming.getStoriesClaimedByOthers();
|
|
595
|
-
if (othersResult.ok && othersResult.stories && othersResult.stories.length > 0) {
|
|
596
|
-
content += `\n${C.amber}${C.bold}═══ 🔒 Claimed Stories ═══${C.reset}\n`;
|
|
597
|
-
content += `${C.dim}Stories locked by other sessions - pick a different one${C.reset}\n`;
|
|
598
|
-
othersResult.stories.forEach(story => {
|
|
599
|
-
const sessionDir = story.claimedBy?.path
|
|
600
|
-
? path.basename(story.claimedBy.path)
|
|
601
|
-
: 'unknown';
|
|
602
|
-
content += ` ${C.coral}🔒${C.reset} ${C.lavender}${story.id}${C.reset} "${story.title}" ${C.dim}→ Session ${story.claimedBy?.session_id || '?'} (${sessionDir})${C.reset}\n`;
|
|
603
|
-
});
|
|
604
|
-
content += '\n';
|
|
605
|
-
}
|
|
823
|
+
if (shouldLoadClaims) {
|
|
824
|
+
const storyClaimingPath = path.join(__dirname, 'lib', 'story-claiming.js');
|
|
825
|
+
const altStoryClaimingPath = '.agileflow/scripts/lib/story-claiming.js';
|
|
606
826
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
827
|
+
if (fs.existsSync(storyClaimingPath) || fs.existsSync(altStoryClaimingPath)) {
|
|
828
|
+
try {
|
|
829
|
+
const claimPath = fs.existsSync(storyClaimingPath)
|
|
830
|
+
? storyClaimingPath
|
|
831
|
+
: altStoryClaimingPath;
|
|
832
|
+
const storyClaiming = require(claimPath);
|
|
833
|
+
|
|
834
|
+
// Get stories claimed by other sessions
|
|
835
|
+
const othersResult = storyClaiming.getStoriesClaimedByOthers();
|
|
836
|
+
if (othersResult.ok && othersResult.stories && othersResult.stories.length > 0) {
|
|
837
|
+
content += `\n${C.amber}${C.bold}═══ 🔒 Claimed Stories ═══${C.reset}\n`;
|
|
838
|
+
content += `${C.dim}Stories locked by other sessions - pick a different one${C.reset}\n`;
|
|
839
|
+
othersResult.stories.forEach(story => {
|
|
840
|
+
const sessionDir = story.claimedBy?.path
|
|
841
|
+
? path.basename(story.claimedBy.path)
|
|
842
|
+
: 'unknown';
|
|
843
|
+
content += ` ${C.coral}🔒${C.reset} ${C.lavender}${story.id}${C.reset} "${story.title}" ${C.dim}→ Session ${story.claimedBy?.session_id || '?'} (${sessionDir})${C.reset}\n`;
|
|
844
|
+
});
|
|
845
|
+
content += '\n';
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Get stories claimed by THIS session
|
|
849
|
+
const myResult = storyClaiming.getClaimedStoriesForSession();
|
|
850
|
+
if (myResult.ok && myResult.stories && myResult.stories.length > 0) {
|
|
851
|
+
content += `\n${C.mintGreen}${C.bold}═══ ✓ Your Claimed Stories ═══${C.reset}\n`;
|
|
852
|
+
myResult.stories.forEach(story => {
|
|
853
|
+
content += ` ${C.mintGreen}✓${C.reset} ${C.lavender}${story.id}${C.reset} "${story.title}"\n`;
|
|
854
|
+
});
|
|
855
|
+
content += '\n';
|
|
856
|
+
}
|
|
857
|
+
} catch (e) {
|
|
858
|
+
// Story claiming not available or error - silently skip
|
|
615
859
|
}
|
|
616
|
-
} catch (e) {
|
|
617
|
-
// Story claiming not available or error - silently skip
|
|
618
860
|
}
|
|
619
861
|
}
|
|
620
862
|
|
|
621
863
|
// 5b. FILE OVERLAPS (inter-session file awareness)
|
|
622
|
-
|
|
623
|
-
const
|
|
864
|
+
// Lazy loading (US-0093): Only load if sectionsToLoad.fileOverlaps is true
|
|
865
|
+
const shouldLoadOverlaps = prefetched?.sectionsToLoad?.fileOverlaps !== false;
|
|
624
866
|
|
|
625
|
-
if (
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
const fileTracking = require(trackPath);
|
|
629
|
-
|
|
630
|
-
// Get file overlaps with other sessions
|
|
631
|
-
const overlapsResult = fileTracking.getMyFileOverlaps();
|
|
632
|
-
if (overlapsResult.ok && overlapsResult.overlaps && overlapsResult.overlaps.length > 0) {
|
|
633
|
-
content += `\n${C.amber}${C.bold}═══ ⚠️ File Overlaps ═══${C.reset}\n`;
|
|
634
|
-
content += `${C.dim}Files also edited by other sessions - conflicts auto-resolved during merge${C.reset}\n`;
|
|
635
|
-
overlapsResult.overlaps.forEach(overlap => {
|
|
636
|
-
const sessionInfo = overlap.otherSessions
|
|
637
|
-
.map(s => {
|
|
638
|
-
const dir = path.basename(s.path);
|
|
639
|
-
return `Session ${s.id} (${dir})`;
|
|
640
|
-
})
|
|
641
|
-
.join(', ');
|
|
642
|
-
content += ` ${C.amber}⚠${C.reset} ${C.lavender}${overlap.file}${C.reset} ${C.dim}→ ${sessionInfo}${C.reset}\n`;
|
|
643
|
-
});
|
|
644
|
-
content += '\n';
|
|
645
|
-
}
|
|
867
|
+
if (shouldLoadOverlaps) {
|
|
868
|
+
const fileTrackingPath = path.join(__dirname, 'lib', 'file-tracking.js');
|
|
869
|
+
const altFileTrackingPath = '.agileflow/scripts/lib/file-tracking.js';
|
|
646
870
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
871
|
+
if (fs.existsSync(fileTrackingPath) || fs.existsSync(altFileTrackingPath)) {
|
|
872
|
+
try {
|
|
873
|
+
const trackPath = fs.existsSync(fileTrackingPath) ? fileTrackingPath : altFileTrackingPath;
|
|
874
|
+
const fileTracking = require(trackPath);
|
|
875
|
+
|
|
876
|
+
// Get file overlaps with other sessions
|
|
877
|
+
const overlapsResult = fileTracking.getMyFileOverlaps();
|
|
878
|
+
if (overlapsResult.ok && overlapsResult.overlaps && overlapsResult.overlaps.length > 0) {
|
|
879
|
+
content += `\n${C.amber}${C.bold}═══ ⚠️ File Overlaps ═══${C.reset}\n`;
|
|
880
|
+
content += `${C.dim}Files also edited by other sessions - conflicts auto-resolved during merge${C.reset}\n`;
|
|
881
|
+
overlapsResult.overlaps.forEach(overlap => {
|
|
882
|
+
const sessionInfo = overlap.otherSessions
|
|
883
|
+
.map(s => {
|
|
884
|
+
const dir = path.basename(s.path);
|
|
885
|
+
return `Session ${s.id} (${dir})`;
|
|
886
|
+
})
|
|
887
|
+
.join(', ');
|
|
888
|
+
content += ` ${C.amber}⚠${C.reset} ${C.lavender}${overlap.file}${C.reset} ${C.dim}→ ${sessionInfo}${C.reset}\n`;
|
|
659
889
|
});
|
|
660
|
-
if (filesResult.files.length > 5) {
|
|
661
|
-
content += ` ${C.dim}... and ${filesResult.files.length - 5} more${C.reset}\n`;
|
|
662
|
-
}
|
|
663
890
|
content += '\n';
|
|
664
891
|
}
|
|
892
|
+
|
|
893
|
+
// Show files touched by this session
|
|
894
|
+
const { getCurrentSession, getSessionFiles } = fileTracking;
|
|
895
|
+
const currentSession = getCurrentSession();
|
|
896
|
+
if (currentSession) {
|
|
897
|
+
const filesResult = getSessionFiles(currentSession.session_id);
|
|
898
|
+
if (filesResult.ok && filesResult.files && filesResult.files.length > 0) {
|
|
899
|
+
content += `\n${C.skyBlue}${C.bold}═══ 📁 Files Touched This Session ═══${C.reset}\n`;
|
|
900
|
+
content += `${C.dim}${filesResult.files.length} files tracked for conflict detection${C.reset}\n`;
|
|
901
|
+
// Show first 5 files max
|
|
902
|
+
const displayFiles = filesResult.files.slice(0, 5);
|
|
903
|
+
displayFiles.forEach(file => {
|
|
904
|
+
content += ` ${C.dim}•${C.reset} ${file}\n`;
|
|
905
|
+
});
|
|
906
|
+
if (filesResult.files.length > 5) {
|
|
907
|
+
content += ` ${C.dim}... and ${filesResult.files.length - 5} more${C.reset}\n`;
|
|
908
|
+
}
|
|
909
|
+
content += '\n';
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
} catch (e) {
|
|
913
|
+
// File tracking not available or error - silently skip
|
|
665
914
|
}
|
|
666
|
-
} catch (e) {
|
|
667
|
-
// File tracking not available or error - silently skip
|
|
668
915
|
}
|
|
669
916
|
}
|
|
670
917
|
|
|
671
918
|
// 6. VISUAL E2E STATUS (detect from metadata or filesystem)
|
|
672
|
-
const metadata =
|
|
919
|
+
const metadata =
|
|
920
|
+
prefetched?.json?.metadata ?? safeReadJSON('docs/00-meta/agileflow-metadata.json');
|
|
673
921
|
const visualE2eConfig = metadata?.features?.visual_e2e;
|
|
674
922
|
const playwrightExists =
|
|
675
923
|
fs.existsSync('playwright.config.ts') || fs.existsSync('playwright.config.js');
|
|
@@ -702,7 +950,7 @@ function generateFullContent() {
|
|
|
702
950
|
// DOCS STRUCTURE (using vibrant 256-color palette)
|
|
703
951
|
content += `\n${C.skyBlue}${C.bold}═══ Documentation ═══${C.reset}\n`;
|
|
704
952
|
const docsDir = 'docs';
|
|
705
|
-
const docFolders = safeLs(docsDir).filter(f => {
|
|
953
|
+
const docFolders = (prefetched?.dirs?.docs ?? safeLs(docsDir)).filter(f => {
|
|
706
954
|
try {
|
|
707
955
|
return fs.statSync(path.join(docsDir, f)).isDirectory();
|
|
708
956
|
} catch {
|
|
@@ -724,23 +972,32 @@ function generateFullContent() {
|
|
|
724
972
|
}
|
|
725
973
|
|
|
726
974
|
// 6. RESEARCH NOTES - List + Full content of most recent (using vibrant 256-color palette)
|
|
975
|
+
// Lazy loading (US-0093): Full content only loaded for research-related commands
|
|
976
|
+
const shouldLoadResearch = prefetched?.sectionsToLoad?.researchContent !== false;
|
|
727
977
|
content += `\n${C.skyBlue}${C.bold}═══ Research Notes ═══${C.reset}\n`;
|
|
728
978
|
const researchDir = 'docs/10-research';
|
|
729
|
-
const researchFiles =
|
|
979
|
+
const researchFiles =
|
|
980
|
+
prefetched?.researchFiles ??
|
|
981
|
+
safeLs(researchDir)
|
|
982
|
+
.filter(f => f.endsWith('.md') && f !== 'README.md')
|
|
983
|
+
.sort()
|
|
984
|
+
.reverse();
|
|
730
985
|
if (researchFiles.length > 0) {
|
|
731
|
-
researchFiles.sort().reverse();
|
|
732
986
|
content += `${C.dim}───${C.reset} Available Research Notes\n`;
|
|
733
987
|
researchFiles.forEach(file => (content += ` ${C.dim}${file}${C.reset}\n`));
|
|
734
988
|
|
|
735
989
|
const mostRecentFile = researchFiles[0];
|
|
736
990
|
const mostRecentPath = path.join(researchDir, mostRecentFile);
|
|
737
|
-
const mostRecentContent =
|
|
991
|
+
const mostRecentContent =
|
|
992
|
+
prefetched?.mostRecentResearch ?? (shouldLoadResearch ? safeRead(mostRecentPath) : null);
|
|
738
993
|
|
|
739
994
|
if (mostRecentContent) {
|
|
740
995
|
content += `\n${C.mintGreen}📄 Most Recent: ${mostRecentFile}${C.reset}\n`;
|
|
741
996
|
content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
|
|
742
997
|
content += mostRecentContent + '\n';
|
|
743
998
|
content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
|
|
999
|
+
} else if (!shouldLoadResearch) {
|
|
1000
|
+
content += `\n${C.dim}📄 Content deferred (lazy loading). Use /agileflow:research to access.${C.reset}\n`;
|
|
744
1001
|
}
|
|
745
1002
|
} else {
|
|
746
1003
|
content += `${C.dim}No research notes${C.reset}\n`;
|
|
@@ -749,7 +1006,7 @@ function generateFullContent() {
|
|
|
749
1006
|
// 7. BUS MESSAGES (using vibrant 256-color palette)
|
|
750
1007
|
content += `\n${C.skyBlue}${C.bold}═══ Recent Agent Messages ═══${C.reset}\n`;
|
|
751
1008
|
const busPath = 'docs/09-agents/bus/log.jsonl';
|
|
752
|
-
const busContent = safeRead(busPath);
|
|
1009
|
+
const busContent = prefetched?.text?.busLog ?? safeRead(busPath);
|
|
753
1010
|
if (busContent) {
|
|
754
1011
|
const lines = busContent.trim().split('\n').filter(Boolean);
|
|
755
1012
|
const recent = lines.slice(-5);
|
|
@@ -773,6 +1030,15 @@ function generateFullContent() {
|
|
|
773
1030
|
// 8. KEY FILES - Full content
|
|
774
1031
|
content += `\n${C.cyan}${C.bold}═══ Key Context Files (Full Content) ═══${C.reset}\n`;
|
|
775
1032
|
|
|
1033
|
+
// Map file paths to prefetched keys
|
|
1034
|
+
const prefetchedKeyMap = {
|
|
1035
|
+
'CLAUDE.md': 'claudeMd',
|
|
1036
|
+
'README.md': 'readmeMd',
|
|
1037
|
+
'docs/04-architecture/README.md': 'archReadme',
|
|
1038
|
+
'docs/02-practices/README.md': 'practicesReadme',
|
|
1039
|
+
'docs/08-project/roadmap.md': 'roadmap',
|
|
1040
|
+
};
|
|
1041
|
+
|
|
776
1042
|
const keyFilesToRead = [
|
|
777
1043
|
{ path: 'CLAUDE.md', label: 'CLAUDE.md (Project Instructions)' },
|
|
778
1044
|
{ path: 'README.md', label: 'README.md (Project Overview)' },
|
|
@@ -782,7 +1048,8 @@ function generateFullContent() {
|
|
|
782
1048
|
];
|
|
783
1049
|
|
|
784
1050
|
keyFilesToRead.forEach(({ path: filePath, label }) => {
|
|
785
|
-
const
|
|
1051
|
+
const prefetchKey = prefetchedKeyMap[filePath];
|
|
1052
|
+
const fileContent = prefetched?.text?.[prefetchKey] ?? safeRead(filePath);
|
|
786
1053
|
if (fileContent) {
|
|
787
1054
|
content += `\n${C.green}✓ ${label}${C.reset} ${C.dim}(${filePath})${C.reset}\n`;
|
|
788
1055
|
content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
|
|
@@ -798,7 +1065,9 @@ function generateFullContent() {
|
|
|
798
1065
|
|
|
799
1066
|
// 9. EPICS FOLDER
|
|
800
1067
|
content += `\n${C.cyan}${C.bold}═══ Epic Files ═══${C.reset}\n`;
|
|
801
|
-
const epicFiles =
|
|
1068
|
+
const epicFiles =
|
|
1069
|
+
prefetched?.dirs?.epics?.filter(f => f.endsWith('.md') && f !== 'README.md') ??
|
|
1070
|
+
safeLs('docs/05-epics').filter(f => f.endsWith('.md') && f !== 'README.md');
|
|
802
1071
|
if (epicFiles.length > 0) {
|
|
803
1072
|
epicFiles.forEach(file => (content += ` ${C.dim}${file}${C.reset}\n`));
|
|
804
1073
|
} else {
|
|
@@ -816,22 +1085,57 @@ function generateFullContent() {
|
|
|
816
1085
|
// MAIN: Output with smart summary positioning
|
|
817
1086
|
// ============================================
|
|
818
1087
|
|
|
819
|
-
|
|
820
|
-
|
|
1088
|
+
/**
|
|
1089
|
+
* Main execution function using parallel pre-fetching for optimal performance.
|
|
1090
|
+
*/
|
|
1091
|
+
async function main() {
|
|
1092
|
+
// Check for multi-session environment before prefetching
|
|
1093
|
+
const registryPath = '.agileflow/sessions/registry.json';
|
|
1094
|
+
let isMultiSession = false;
|
|
1095
|
+
if (fs.existsSync(registryPath)) {
|
|
1096
|
+
try {
|
|
1097
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
1098
|
+
const sessionCount = Object.keys(registry.sessions || {}).length;
|
|
1099
|
+
isMultiSession = sessionCount > 1;
|
|
1100
|
+
} catch {
|
|
1101
|
+
// Ignore registry read errors
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
821
1104
|
|
|
822
|
-
|
|
823
|
-
const
|
|
1105
|
+
// Load lazy context configuration from metadata
|
|
1106
|
+
const metadata = safeReadJSON('docs/00-meta/agileflow-metadata.json');
|
|
1107
|
+
const lazyConfig = metadata?.features?.lazyContext;
|
|
824
1108
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
console.log(fullContent);
|
|
828
|
-
console.log(summary);
|
|
829
|
-
} else {
|
|
830
|
-
// Output content up to cutoff, then summary as the LAST visible thing.
|
|
831
|
-
// Don't output contentAfter - it would bleed into visible area before truncation,
|
|
832
|
-
// and Claude only sees ~30K chars from Bash anyway.
|
|
833
|
-
const contentBefore = fullContent.substring(0, cutoffPoint);
|
|
1109
|
+
// Determine which sections need full content (US-0093)
|
|
1110
|
+
const sectionsToLoad = determineSectionsToLoad(commandName, lazyConfig, isMultiSession);
|
|
834
1111
|
|
|
835
|
-
|
|
836
|
-
|
|
1112
|
+
// Pre-fetch all file data in parallel with lazy loading options
|
|
1113
|
+
const prefetched = await prefetchAllData({ sectionsToLoad });
|
|
1114
|
+
|
|
1115
|
+
// Generate content using pre-fetched data
|
|
1116
|
+
const summary = generateSummary(prefetched);
|
|
1117
|
+
const fullContent = generateFullContent(prefetched);
|
|
1118
|
+
|
|
1119
|
+
const summaryLength = summary.length;
|
|
1120
|
+
const cutoffPoint = DISPLAY_LIMIT - summaryLength;
|
|
1121
|
+
|
|
1122
|
+
if (fullContent.length <= cutoffPoint) {
|
|
1123
|
+
// Full content fits before summary - just output everything
|
|
1124
|
+
console.log(fullContent);
|
|
1125
|
+
console.log(summary);
|
|
1126
|
+
} else {
|
|
1127
|
+
// Output content up to cutoff, then summary as the LAST visible thing.
|
|
1128
|
+
// Don't output contentAfter - it would bleed into visible area before truncation,
|
|
1129
|
+
// and Claude only sees ~30K chars from Bash anyway.
|
|
1130
|
+
const contentBefore = fullContent.substring(0, cutoffPoint);
|
|
1131
|
+
|
|
1132
|
+
console.log(contentBefore);
|
|
1133
|
+
console.log(summary);
|
|
1134
|
+
}
|
|
837
1135
|
}
|
|
1136
|
+
|
|
1137
|
+
// Execute main function
|
|
1138
|
+
main().catch(err => {
|
|
1139
|
+
console.error('Error gathering context:', err.message);
|
|
1140
|
+
process.exit(1);
|
|
1141
|
+
});
|