agileflow 2.91.0 → 2.92.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +3 -3
  3. package/lib/README.md +178 -0
  4. package/lib/codebase-indexer.js +31 -23
  5. package/lib/colors.js +190 -12
  6. package/lib/consent.js +232 -0
  7. package/lib/correlation.js +277 -0
  8. package/lib/error-codes.js +46 -0
  9. package/lib/errors.js +48 -6
  10. package/lib/file-cache.js +182 -0
  11. package/lib/format-error.js +156 -0
  12. package/lib/path-resolver.js +155 -7
  13. package/lib/paths.js +212 -20
  14. package/lib/placeholder-registry.js +205 -0
  15. package/lib/registry-di.js +358 -0
  16. package/lib/result-schema.js +363 -0
  17. package/lib/result.js +210 -0
  18. package/lib/session-registry.js +13 -0
  19. package/lib/session-state-machine.js +465 -0
  20. package/lib/validate-commands.js +308 -0
  21. package/lib/validate.js +116 -52
  22. package/package.json +1 -1
  23. package/scripts/af +34 -0
  24. package/scripts/agent-loop.js +63 -9
  25. package/scripts/agileflow-configure.js +2 -2
  26. package/scripts/agileflow-welcome.js +435 -23
  27. package/scripts/archive-completed-stories.sh +57 -11
  28. package/scripts/claude-tmux.sh +102 -0
  29. package/scripts/damage-control-bash.js +3 -70
  30. package/scripts/damage-control-edit.js +3 -20
  31. package/scripts/damage-control-write.js +3 -20
  32. package/scripts/dependency-check.js +310 -0
  33. package/scripts/get-env.js +11 -4
  34. package/scripts/lib/configure-detect.js +23 -1
  35. package/scripts/lib/configure-features.js +43 -2
  36. package/scripts/lib/context-formatter.js +771 -0
  37. package/scripts/lib/context-loader.js +699 -0
  38. package/scripts/lib/damage-control-utils.js +107 -0
  39. package/scripts/lib/json-utils.sh +162 -0
  40. package/scripts/lib/state-migrator.js +353 -0
  41. package/scripts/lib/story-state-machine.js +437 -0
  42. package/scripts/obtain-context.js +80 -1248
  43. package/scripts/pre-push-check.sh +46 -0
  44. package/scripts/precompact-context.sh +23 -10
  45. package/scripts/query-codebase.js +122 -14
  46. package/scripts/ralph-loop.js +5 -5
  47. package/scripts/session-manager.js +220 -42
  48. package/scripts/spawn-parallel.js +651 -0
  49. package/scripts/tui/blessed/data/watcher.js +20 -15
  50. package/scripts/tui/blessed/index.js +2 -2
  51. package/scripts/tui/blessed/panels/output.js +14 -8
  52. package/scripts/tui/blessed/panels/sessions.js +22 -15
  53. package/scripts/tui/blessed/panels/trace.js +14 -8
  54. package/scripts/tui/blessed/ui/help.js +3 -3
  55. package/scripts/tui/blessed/ui/screen.js +4 -4
  56. package/scripts/tui/blessed/ui/statusbar.js +5 -9
  57. package/scripts/tui/blessed/ui/tabbar.js +11 -11
  58. package/scripts/validators/component-validator.js +41 -14
  59. package/scripts/validators/json-schema-validator.js +11 -4
  60. package/scripts/validators/markdown-validator.js +1 -2
  61. package/scripts/validators/migration-validator.js +17 -5
  62. package/scripts/validators/security-validator.js +137 -33
  63. package/scripts/validators/story-format-validator.js +31 -10
  64. package/scripts/validators/test-result-validator.js +19 -4
  65. package/scripts/validators/workflow-validator.js +12 -5
  66. package/src/core/agents/codebase-query.md +24 -0
  67. package/src/core/commands/adr.md +114 -0
  68. package/src/core/commands/agent.md +120 -0
  69. package/src/core/commands/assign.md +145 -0
  70. package/src/core/commands/babysit.md +32 -5
  71. package/src/core/commands/changelog.md +118 -0
  72. package/src/core/commands/configure.md +42 -6
  73. package/src/core/commands/diagnose.md +114 -0
  74. package/src/core/commands/epic.md +113 -0
  75. package/src/core/commands/handoff.md +128 -0
  76. package/src/core/commands/help.md +75 -0
  77. package/src/core/commands/pr.md +96 -0
  78. package/src/core/commands/roadmap/analyze.md +400 -0
  79. package/src/core/commands/session/new.md +113 -6
  80. package/src/core/commands/session/spawn.md +197 -0
  81. package/src/core/commands/sprint.md +22 -0
  82. package/src/core/commands/status.md +74 -0
  83. package/src/core/commands/story.md +143 -4
  84. package/src/core/templates/agileflow-metadata.json +55 -2
  85. package/src/core/templates/plan-template.md +125 -0
  86. package/src/core/templates/story-lifecycle.md +213 -0
  87. package/src/core/templates/story-template.md +4 -0
  88. package/src/core/templates/tdd-test-template.js +241 -0
  89. package/tools/cli/commands/setup.js +86 -0
  90. package/tools/cli/installers/core/installer.js +94 -0
  91. package/tools/cli/installers/ide/_base-ide.js +20 -11
  92. package/tools/cli/installers/ide/codex.js +29 -47
  93. package/tools/cli/lib/config-manager.js +17 -2
  94. package/tools/cli/lib/content-transformer.js +271 -0
  95. package/tools/cli/lib/error-handler.js +14 -22
  96. package/tools/cli/lib/ide-error-factory.js +421 -0
  97. package/tools/cli/lib/ide-health-monitor.js +364 -0
  98. package/tools/cli/lib/ide-registry.js +114 -1
  99. package/tools/cli/lib/ui.js +14 -25
@@ -0,0 +1,699 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * context-loader.js
4
+ *
5
+ * Data loading module for obtain-context.js (US-0148)
6
+ *
7
+ * Responsibilities:
8
+ * - Synchronous and asynchronous file/JSON/directory reading
9
+ * - Git command execution
10
+ * - Parallel pre-fetching of all required data
11
+ * - Context budget tracking from Claude session files
12
+ * - Lazy loading configuration and section determination
13
+ * - Command argument parsing
14
+ *
15
+ * Performance optimization: Uses Promise.all() for parallel I/O (US-0092)
16
+ * Lazy evaluation: Conditionally loads sections based on command (US-0093)
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const fsPromises = require('fs').promises;
21
+ const path = require('path');
22
+ const os = require('os');
23
+ const { execSync, exec } = require('child_process');
24
+
25
+ // Try to use cached reads if available
26
+ let readJSONCached, readFileCached;
27
+ try {
28
+ const fileCache = require('../../lib/file-cache');
29
+ readJSONCached = fileCache.readJSONCached;
30
+ readFileCached = fileCache.readFileCached;
31
+ } catch {
32
+ // Fallback if file-cache not available
33
+ readJSONCached = null;
34
+ readFileCached = null;
35
+ }
36
+
37
+ // =============================================================================
38
+ // Command Whitelist for safeExec (US-0120)
39
+ // =============================================================================
40
+
41
+ /**
42
+ * Whitelisted commands for safeExec
43
+ * Only commands starting with these prefixes are allowed
44
+ */
45
+ const SAFEEXEC_ALLOWED_COMMANDS = [
46
+ // Git commands (read-only operations)
47
+ 'git ',
48
+ 'git branch',
49
+ 'git log',
50
+ 'git status',
51
+ 'git diff',
52
+ 'git rev-parse',
53
+ 'git describe',
54
+ 'git show',
55
+ 'git config',
56
+ 'git remote',
57
+ 'git tag',
58
+ // Node commands (for internal AgileFlow scripts only)
59
+ 'node ',
60
+ ];
61
+
62
+ /**
63
+ * Dangerous patterns that should never be executed
64
+ */
65
+ const SAFEEXEC_BLOCKED_PATTERNS = [
66
+ /\|/, // Pipe
67
+ /;/, // Command separator
68
+ /&&/, // AND operator
69
+ /\|\|/, // OR operator
70
+ /`/, // Backticks
71
+ /\$\(/, // Command substitution
72
+ />/, // Redirect output
73
+ /</, // Redirect input
74
+ /\bsudo\b/, // Sudo
75
+ /\brm\b/, // Remove
76
+ /\bmv\b/, // Move
77
+ /\bcp\b/, // Copy
78
+ /\bchmod\b/, // Change permissions
79
+ /\bchown\b/, // Change owner
80
+ /\bcurl\b/, // curl (network)
81
+ /\bwget\b/, // wget (network)
82
+ ];
83
+
84
+ /**
85
+ * Logger for safeExec operations (configurable)
86
+ */
87
+ let _safeExecLogger = null;
88
+
89
+ /**
90
+ * Configure the safeExec logger
91
+ * @param {Function|null} logger - Logger function or null to disable
92
+ */
93
+ function configureSafeExecLogger(logger) {
94
+ _safeExecLogger = logger;
95
+ }
96
+
97
+ /**
98
+ * Log a safeExec operation
99
+ * @param {string} level - Log level ('debug', 'warn', 'error')
100
+ * @param {string} message - Log message
101
+ * @param {Object} [details] - Additional details
102
+ */
103
+ function logSafeExec(level, message, details = {}) {
104
+ if (_safeExecLogger) {
105
+ _safeExecLogger(level, message, details);
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Check if a command is allowed
111
+ * @param {string} cmd - Command to check
112
+ * @returns {{allowed: boolean, reason?: string}}
113
+ */
114
+ function isCommandAllowed(cmd) {
115
+ if (!cmd || typeof cmd !== 'string') {
116
+ return { allowed: false, reason: 'Invalid command' };
117
+ }
118
+
119
+ const trimmed = cmd.trim();
120
+
121
+ // Check for blocked patterns
122
+ for (const pattern of SAFEEXEC_BLOCKED_PATTERNS) {
123
+ if (pattern.test(trimmed)) {
124
+ return { allowed: false, reason: `Blocked pattern: ${pattern}` };
125
+ }
126
+ }
127
+
128
+ // Check against whitelist
129
+ const isWhitelisted = SAFEEXEC_ALLOWED_COMMANDS.some(prefix => trimmed.startsWith(prefix));
130
+
131
+ if (!isWhitelisted) {
132
+ return { allowed: false, reason: 'Command not in whitelist' };
133
+ }
134
+
135
+ return { allowed: true };
136
+ }
137
+
138
+ // =============================================================================
139
+ // Synchronous I/O Helpers
140
+ // =============================================================================
141
+
142
+ /**
143
+ * Safely read a file, returning null on error.
144
+ * @param {string} filePath - Path to file
145
+ * @returns {string|null} File contents or null
146
+ */
147
+ function safeRead(filePath) {
148
+ try {
149
+ return fs.readFileSync(filePath, 'utf8');
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Safely read and parse JSON file, using cache when available.
157
+ * @param {string} filePath - Path to JSON file
158
+ * @returns {Object|null} Parsed JSON or null
159
+ */
160
+ function safeReadJSON(filePath) {
161
+ if (readJSONCached) {
162
+ const absPath = path.resolve(filePath);
163
+ return readJSONCached(absPath);
164
+ }
165
+ try {
166
+ const content = fs.readFileSync(filePath, 'utf8');
167
+ return JSON.parse(content);
168
+ } catch {
169
+ return null;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Safely list directory contents.
175
+ * @param {string} dirPath - Directory path
176
+ * @returns {string[]} Array of filenames or empty array
177
+ */
178
+ function safeLs(dirPath) {
179
+ try {
180
+ return fs.readdirSync(dirPath);
181
+ } catch {
182
+ return [];
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Safely execute a shell command with whitelist validation.
188
+ *
189
+ * Only whitelisted commands (mainly git operations) are allowed.
190
+ * Dangerous patterns (pipes, redirects, etc.) are blocked.
191
+ *
192
+ * @param {string} cmd - Command to execute
193
+ * @param {Object} [options] - Options
194
+ * @param {boolean} [options.bypassWhitelist=false] - Skip whitelist check (use with caution)
195
+ * @returns {string|null} Command output or null
196
+ */
197
+ function safeExec(cmd, options = {}) {
198
+ const { bypassWhitelist = false } = options;
199
+
200
+ // Validate command unless bypassed
201
+ if (!bypassWhitelist) {
202
+ const check = isCommandAllowed(cmd);
203
+ if (!check.allowed) {
204
+ logSafeExec('warn', 'Command blocked by whitelist', {
205
+ cmd: cmd?.substring(0, 100),
206
+ reason: check.reason,
207
+ });
208
+ return null;
209
+ }
210
+ }
211
+
212
+ logSafeExec('debug', 'Executing command', {
213
+ cmd: cmd?.substring(0, 100),
214
+ bypassed: bypassWhitelist,
215
+ });
216
+
217
+ try {
218
+ const result = execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
219
+ logSafeExec('debug', 'Command succeeded', {
220
+ cmd: cmd?.substring(0, 50),
221
+ outputLength: result?.length || 0,
222
+ });
223
+ return result;
224
+ } catch (error) {
225
+ logSafeExec('debug', 'Command failed', {
226
+ cmd: cmd?.substring(0, 50),
227
+ error: error?.message?.substring(0, 100),
228
+ });
229
+ return null;
230
+ }
231
+ }
232
+
233
+ // =============================================================================
234
+ // Asynchronous I/O Helpers
235
+ // =============================================================================
236
+
237
+ /**
238
+ * Asynchronously read a file.
239
+ * @param {string} filePath - Path to file
240
+ * @returns {Promise<string|null>} File contents or null
241
+ */
242
+ async function safeReadAsync(filePath) {
243
+ try {
244
+ return await fsPromises.readFile(filePath, 'utf8');
245
+ } catch {
246
+ return null;
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Asynchronously read and parse JSON file.
252
+ * @param {string} filePath - Path to JSON file
253
+ * @returns {Promise<Object|null>} Parsed JSON or null
254
+ */
255
+ async function safeReadJSONAsync(filePath) {
256
+ try {
257
+ const content = await fsPromises.readFile(filePath, 'utf8');
258
+ return JSON.parse(content);
259
+ } catch {
260
+ return null;
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Asynchronously list directory contents.
266
+ * @param {string} dirPath - Directory path
267
+ * @returns {Promise<string[]>} Array of filenames or empty array
268
+ */
269
+ async function safeLsAsync(dirPath) {
270
+ try {
271
+ return await fsPromises.readdir(dirPath);
272
+ } catch {
273
+ return [];
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Execute a command asynchronously with whitelist validation.
279
+ *
280
+ * Only whitelisted commands (mainly git operations) are allowed.
281
+ * Dangerous patterns (pipes, redirects, etc.) are blocked.
282
+ *
283
+ * @param {string} cmd - Command to execute
284
+ * @param {Object} [options] - Options
285
+ * @param {boolean} [options.bypassWhitelist=false] - Skip whitelist check (use with caution)
286
+ * @returns {Promise<string|null>} Command output or null
287
+ */
288
+ async function safeExecAsync(cmd, options = {}) {
289
+ const { bypassWhitelist = false } = options;
290
+
291
+ // Validate command unless bypassed
292
+ if (!bypassWhitelist) {
293
+ const check = isCommandAllowed(cmd);
294
+ if (!check.allowed) {
295
+ logSafeExec('warn', 'Async command blocked by whitelist', {
296
+ cmd: cmd?.substring(0, 100),
297
+ reason: check.reason,
298
+ });
299
+ return null;
300
+ }
301
+ }
302
+
303
+ logSafeExec('debug', 'Executing async command', {
304
+ cmd: cmd?.substring(0, 100),
305
+ bypassed: bypassWhitelist,
306
+ });
307
+
308
+ return new Promise(resolve => {
309
+ exec(cmd, { encoding: 'utf8' }, (error, stdout) => {
310
+ if (error) {
311
+ logSafeExec('debug', 'Async command failed', {
312
+ cmd: cmd?.substring(0, 50),
313
+ error: error?.message?.substring(0, 100),
314
+ });
315
+ resolve(null);
316
+ } else {
317
+ const result = stdout.trim();
318
+ logSafeExec('debug', 'Async command succeeded', {
319
+ cmd: cmd?.substring(0, 50),
320
+ outputLength: result?.length || 0,
321
+ });
322
+ resolve(result);
323
+ }
324
+ });
325
+ });
326
+ }
327
+
328
+ // =============================================================================
329
+ // Context Budget Tracking (GSD Integration)
330
+ // =============================================================================
331
+
332
+ /**
333
+ * Get current context usage percentage from Claude's session files.
334
+ * Reads token counts from the active session JSONL file.
335
+ *
336
+ * @returns {{ percent: number, tokens: number, max: number } | null}
337
+ */
338
+ function getContextPercentage() {
339
+ try {
340
+ const homeDir = os.homedir();
341
+ const cwd = process.cwd();
342
+
343
+ // Convert current dir to Claude's session file path format
344
+ const projectDir = cwd
345
+ .replace(homeDir, '~')
346
+ .replace('~', homeDir)
347
+ .replace(/\//g, '-')
348
+ .replace(/^-/, '');
349
+ const sessionDir = path.join(homeDir, '.claude', 'projects', `-${projectDir}`);
350
+
351
+ if (!fs.existsSync(sessionDir)) {
352
+ return null;
353
+ }
354
+
355
+ // Find most recent .jsonl session file
356
+ const files = fs
357
+ .readdirSync(sessionDir)
358
+ .filter(f => f.endsWith('.jsonl'))
359
+ .map(f => ({
360
+ name: f,
361
+ mtime: fs.statSync(path.join(sessionDir, f)).mtime.getTime(),
362
+ }))
363
+ .sort((a, b) => b.mtime - a.mtime);
364
+
365
+ if (files.length === 0) {
366
+ return null;
367
+ }
368
+
369
+ const sessionFile = path.join(sessionDir, files[0].name);
370
+ const content = fs.readFileSync(sessionFile, 'utf8');
371
+ const lines = content.trim().split('\n').slice(-20); // Last 20 lines
372
+
373
+ // Find latest usage entry
374
+ let latestTokens = 0;
375
+ for (const line of lines.reverse()) {
376
+ try {
377
+ const entry = JSON.parse(line);
378
+ if (entry?.message?.usage) {
379
+ const usage = entry.message.usage;
380
+ latestTokens = (usage.input_tokens || 0) + (usage.cache_read_input_tokens || 0);
381
+ if (latestTokens > 0) break;
382
+ }
383
+ } catch {
384
+ // Skip malformed lines
385
+ }
386
+ }
387
+
388
+ if (latestTokens === 0) {
389
+ return null;
390
+ }
391
+
392
+ // Default to 200K context for modern Claude models
393
+ const maxContext = 200000;
394
+ const percent = Math.min(100, Math.round((latestTokens * 100) / maxContext));
395
+
396
+ return { percent, tokens: latestTokens, max: maxContext };
397
+ } catch {
398
+ return null;
399
+ }
400
+ }
401
+
402
+ // =============================================================================
403
+ // Lazy Evaluation Configuration (US-0093)
404
+ // =============================================================================
405
+
406
+ /**
407
+ * Commands that need full research notes content
408
+ */
409
+ const RESEARCH_COMMANDS = ['research', 'ideate', 'mentor', 'rpi'];
410
+
411
+ /**
412
+ * Determine which sections need to be loaded based on command and environment.
413
+ *
414
+ * @param {string} cmdName - Command name being executed
415
+ * @param {Object} lazyConfig - Lazy context configuration from metadata
416
+ * @param {boolean} isMultiSession - Whether multiple sessions are detected
417
+ * @returns {Object} Sections to load { researchContent, sessionClaims, fileOverlaps }
418
+ */
419
+ function determineSectionsToLoad(cmdName, lazyConfig, isMultiSession) {
420
+ // If lazy loading is disabled, load everything
421
+ if (!lazyConfig?.enabled) {
422
+ return {
423
+ researchContent: true,
424
+ sessionClaims: true,
425
+ fileOverlaps: true,
426
+ };
427
+ }
428
+
429
+ // Research notes: load for research-related commands or if 'always'
430
+ const needsResearch =
431
+ lazyConfig.researchNotes === 'always' ||
432
+ (lazyConfig.researchNotes === 'conditional' && RESEARCH_COMMANDS.includes(cmdName));
433
+
434
+ // Session claims: load if multi-session environment or if 'always'
435
+ const needsClaims =
436
+ lazyConfig.sessionClaims === 'always' ||
437
+ (lazyConfig.sessionClaims === 'conditional' && isMultiSession);
438
+
439
+ // File overlaps: load if multi-session environment or if 'always'
440
+ const needsOverlaps =
441
+ lazyConfig.fileOverlaps === 'always' ||
442
+ (lazyConfig.fileOverlaps === 'conditional' && isMultiSession);
443
+
444
+ return {
445
+ researchContent: needsResearch,
446
+ sessionClaims: needsClaims,
447
+ fileOverlaps: needsOverlaps,
448
+ };
449
+ }
450
+
451
+ // =============================================================================
452
+ // Command Argument Parsing
453
+ // =============================================================================
454
+
455
+ /**
456
+ * Parse command-line arguments and determine which sections to activate.
457
+ *
458
+ * @param {string[]} args - Command-line arguments after command name
459
+ * @returns {Object} { activeSections: string[], params: Object }
460
+ */
461
+ function parseCommandArgs(args) {
462
+ const activeSections = [];
463
+ const params = {};
464
+
465
+ for (const arg of args) {
466
+ // Parse KEY=VALUE arguments
467
+ const match = arg.match(/^([A-Z_]+)=(.+)$/i);
468
+ if (match) {
469
+ const [, key, value] = match;
470
+ params[key.toUpperCase()] = value;
471
+ }
472
+ }
473
+
474
+ // Activate sections based on parameters
475
+ if (params.MODE === 'loop') {
476
+ activeSections.push('loop-mode');
477
+ }
478
+
479
+ if (params.VISUAL === 'true') {
480
+ activeSections.push('visual-e2e');
481
+ }
482
+
483
+ // Query mode: QUERY=<pattern> triggers targeted codebase search (US-0127)
484
+ if (params.QUERY) {
485
+ activeSections.push('query-mode');
486
+ }
487
+
488
+ // Check for multi-session environment
489
+ const registryPath = '.agileflow/sessions/registry.json';
490
+ if (fs.existsSync(registryPath)) {
491
+ try {
492
+ const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
493
+ const sessionCount = Object.keys(registry.sessions || {}).length;
494
+ if (sessionCount > 1) {
495
+ activeSections.push('multi-session');
496
+ }
497
+ } catch {
498
+ // Silently ignore registry read errors
499
+ }
500
+ }
501
+
502
+ return { activeSections, params };
503
+ }
504
+
505
+ /**
506
+ * Extract command type from frontmatter (output-only vs interactive).
507
+ *
508
+ * @param {string} cmdName - Command name
509
+ * @returns {string} Command type ('interactive', 'output-only', etc.)
510
+ */
511
+ function getCommandType(cmdName) {
512
+ // Handle nested command paths like "research/ask" -> "research/ask.md"
513
+ const cmdPath = cmdName.includes('/')
514
+ ? `${cmdName.substring(0, cmdName.lastIndexOf('/'))}/${cmdName.substring(cmdName.lastIndexOf('/') + 1)}.md`
515
+ : `${cmdName}.md`;
516
+
517
+ const possiblePaths = [
518
+ `packages/cli/src/core/commands/${cmdPath}`,
519
+ `.agileflow/commands/${cmdPath}`,
520
+ `.claude/commands/agileflow/${cmdPath}`,
521
+ `packages/cli/src/core/commands/${cmdName.replace(/\//g, '-')}.md`,
522
+ ];
523
+
524
+ for (const searchPath of possiblePaths) {
525
+ if (fs.existsSync(searchPath)) {
526
+ try {
527
+ const content = fs.readFileSync(searchPath, 'utf8');
528
+ const match = content.match(/^---\n[\s\S]*?type:\s*(\S+)/m);
529
+ if (match) {
530
+ return match[1].replace(/['"]/g, '');
531
+ }
532
+ } catch {
533
+ // Continue to next path
534
+ }
535
+ }
536
+ }
537
+ return 'interactive';
538
+ }
539
+
540
+ // =============================================================================
541
+ // Parallel Data Pre-fetching
542
+ // =============================================================================
543
+
544
+ /**
545
+ * Pre-fetch all required data in parallel for optimal performance.
546
+ * This dramatically reduces I/O wait time by overlapping file reads and git commands.
547
+ *
548
+ * @param {Object} options - Options for prefetching
549
+ * @param {Object} options.sectionsToLoad - Which sections need full content
550
+ * @returns {Promise<Object>} Pre-fetched data for content generation
551
+ */
552
+ async function prefetchAllData(options = {}) {
553
+ const sectionsToLoad = options.sectionsToLoad || {
554
+ researchContent: true,
555
+ sessionClaims: true,
556
+ fileOverlaps: true,
557
+ };
558
+
559
+ // Define all files to read
560
+ const jsonFiles = {
561
+ metadata: 'docs/00-meta/agileflow-metadata.json',
562
+ statusJson: 'docs/09-agents/status.json',
563
+ sessionState: 'docs/09-agents/session-state.json',
564
+ };
565
+
566
+ const textFiles = {
567
+ busLog: 'docs/09-agents/bus/log.jsonl',
568
+ claudeMd: 'CLAUDE.md',
569
+ readmeMd: 'README.md',
570
+ archReadme: 'docs/04-architecture/README.md',
571
+ practicesReadme: 'docs/02-practices/README.md',
572
+ roadmap: 'docs/08-project/roadmap.md',
573
+ };
574
+
575
+ const directories = {
576
+ docs: 'docs',
577
+ research: 'docs/10-research',
578
+ epics: 'docs/05-epics',
579
+ };
580
+
581
+ // Git commands to run in parallel
582
+ const gitCommands = {
583
+ branch: 'git branch --show-current',
584
+ commitShort: 'git log -1 --format="%h"',
585
+ commitMsg: 'git log -1 --format="%s"',
586
+ commitFull: 'git log -1 --format="%h %s"',
587
+ status: 'git status --short',
588
+ };
589
+
590
+ // Create all promises for parallel execution
591
+ const jsonPromises = Object.entries(jsonFiles).map(async ([key, filePath]) => {
592
+ const data = await safeReadJSONAsync(filePath);
593
+ return [key, data];
594
+ });
595
+
596
+ const textPromises = Object.entries(textFiles).map(async ([key, filePath]) => {
597
+ const data = await safeReadAsync(filePath);
598
+ return [key, data];
599
+ });
600
+
601
+ const dirPromises = Object.entries(directories).map(async ([key, dirPath]) => {
602
+ const files = await safeLsAsync(dirPath);
603
+ return [key, files];
604
+ });
605
+
606
+ const gitPromises = Object.entries(gitCommands).map(async ([key, cmd]) => {
607
+ const data = await safeExecAsync(cmd);
608
+ return [key, data];
609
+ });
610
+
611
+ // Execute all I/O operations in parallel
612
+ const [jsonResults, textResults, dirResults, gitResults] = await Promise.all([
613
+ Promise.all(jsonPromises),
614
+ Promise.all(textPromises),
615
+ Promise.all(dirPromises),
616
+ Promise.all(gitPromises),
617
+ ]);
618
+
619
+ // Convert arrays back to objects
620
+ const json = Object.fromEntries(jsonResults);
621
+ const text = Object.fromEntries(textResults);
622
+ const dirs = Object.fromEntries(dirResults);
623
+ const git = Object.fromEntries(gitResults);
624
+
625
+ // Determine most recent research file
626
+ const researchFiles = dirs.research
627
+ .filter(f => f.endsWith('.md') && f !== 'README.md')
628
+ .sort()
629
+ .reverse();
630
+
631
+ // Lazy loading (US-0093): Only fetch research content if needed
632
+ let mostRecentResearch = null;
633
+ if (sectionsToLoad.researchContent && researchFiles.length > 0) {
634
+ mostRecentResearch = await safeReadAsync(path.join('docs/10-research', researchFiles[0]));
635
+ }
636
+
637
+ return {
638
+ json,
639
+ text,
640
+ dirs,
641
+ git,
642
+ researchFiles,
643
+ mostRecentResearch,
644
+ sectionsToLoad,
645
+ };
646
+ }
647
+
648
+ /**
649
+ * Check if multi-session environment is detected.
650
+ * @returns {boolean} True if multiple sessions exist
651
+ */
652
+ function isMultiSessionEnvironment() {
653
+ const registryPath = '.agileflow/sessions/registry.json';
654
+ if (fs.existsSync(registryPath)) {
655
+ try {
656
+ const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
657
+ const sessionCount = Object.keys(registry.sessions || {}).length;
658
+ return sessionCount > 1;
659
+ } catch {
660
+ return false;
661
+ }
662
+ }
663
+ return false;
664
+ }
665
+
666
+ module.exports = {
667
+ // Sync helpers
668
+ safeRead,
669
+ safeReadJSON,
670
+ safeLs,
671
+ safeExec,
672
+
673
+ // Async helpers
674
+ safeReadAsync,
675
+ safeReadJSONAsync,
676
+ safeLsAsync,
677
+ safeExecAsync,
678
+
679
+ // Command whitelist (US-0120)
680
+ SAFEEXEC_ALLOWED_COMMANDS,
681
+ SAFEEXEC_BLOCKED_PATTERNS,
682
+ configureSafeExecLogger,
683
+ isCommandAllowed,
684
+
685
+ // Context tracking
686
+ getContextPercentage,
687
+
688
+ // Lazy loading
689
+ RESEARCH_COMMANDS,
690
+ determineSectionsToLoad,
691
+
692
+ // Command parsing
693
+ parseCommandArgs,
694
+ getCommandType,
695
+
696
+ // Data prefetching
697
+ prefetchAllData,
698
+ isMultiSessionEnvironment,
699
+ };