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
@@ -2,29 +2,16 @@
2
2
  /**
3
3
  * obtain-context.js
4
4
  *
5
- * Gathers all project context in a single execution for any AgileFlow command or agent.
5
+ * Orchestrator for gathering all project context in a single execution.
6
+ * Refactored in US-0148 to separate concerns:
7
+ * - context-loader.js: Data loading operations
8
+ * - context-formatter.js: Output formatting
9
+ * - obtain-context.js: Orchestration (this file, ~180 lines)
6
10
  *
7
11
  * SMART OUTPUT STRATEGY:
8
12
  * - Calculates summary character count dynamically
9
13
  * - Shows (30K - summary_chars) of full content first
10
14
  * - Then shows the summary (so user sees it at their display cutoff)
11
- * - Then shows rest of full content (for Claude)
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
- *
24
- * QUERY MODE (US-0127):
25
- * - When QUERY=<pattern> provided, uses codebase index for targeted search
26
- * - Falls back to full context if query returns empty
27
- * - Based on RLM pattern: programmatic search instead of loading everything
28
15
  *
29
16
  * Usage:
30
17
  * node scripts/obtain-context.js # Just gather context
@@ -33,1230 +20,93 @@
33
20
  */
34
21
 
35
22
  const fs = require('fs');
36
- const fsPromises = require('fs').promises;
37
23
  const path = require('path');
38
- const os = require('os');
39
24
  const { execSync } = require('child_process');
40
- const { c: C, box } = require('../lib/colors');
41
- const { isValidCommandName } = require('../lib/validate');
42
- const { readJSONCached, readFileCached } = require('../lib/file-cache');
43
25
 
44
- // Claude Code's Bash tool truncates around 30K chars, but ANSI codes and
45
- // box-drawing characters (╭╮╰╯─│) are multi-byte UTF-8, so we need buffer.
46
- // Summary table should be the LAST thing visible before truncation.
47
- const DISPLAY_LIMIT = 29200;
26
+ // Import loader and formatter modules
27
+ const {
28
+ parseCommandArgs,
29
+ getCommandType,
30
+ safeReadJSON,
31
+ prefetchAllData,
32
+ determineSectionsToLoad,
33
+ isMultiSessionEnvironment,
34
+ } = require('./lib/context-loader');
48
35
 
49
- // =============================================================================
50
- // Progressive Disclosure: Section Activation
51
- // =============================================================================
36
+ const { generateSummary, generateFullContent } = require('./lib/context-formatter');
52
37
 
53
- /**
54
- * Parse command-line arguments and determine which sections to activate.
55
- * Sections are conditionally loaded based on parameters like MODE=loop.
56
- *
57
- * Section mapping:
58
- * - MODE=loop → activates: loop-mode
59
- * - Multi-session env → activates: multi-session
60
- * - (Other triggers detected at runtime by the agent)
61
- *
62
- * @param {string[]} args - Command-line arguments after command name
63
- * @returns {Object} { activeSections: string[], params: Object }
64
- */
65
- function parseCommandArgs(args) {
66
- const activeSections = [];
67
- const params = {};
68
-
69
- for (const arg of args) {
70
- // Parse KEY=VALUE arguments
71
- const match = arg.match(/^([A-Z_]+)=(.+)$/i);
72
- if (match) {
73
- const [, key, value] = match;
74
- params[key.toUpperCase()] = value;
75
- }
76
- }
77
-
78
- // Activate sections based on parameters
79
- if (params.MODE === 'loop') {
80
- activeSections.push('loop-mode');
81
- }
82
-
83
- if (params.VISUAL === 'true') {
84
- activeSections.push('visual-e2e');
85
- }
86
-
87
- // Query mode: QUERY=<pattern> triggers targeted codebase search (US-0127)
88
- if (params.QUERY) {
89
- activeSections.push('query-mode');
90
- }
38
+ // Import validation
39
+ let isValidCommandName;
40
+ try {
41
+ isValidCommandName = require('../lib/validate').isValidCommandName;
42
+ } catch {
43
+ isValidCommandName = name => /^[a-z][a-z0-9-]*$/i.test(name);
44
+ }
91
45
 
92
- // Check for multi-session environment
93
- const registryPath = '.agileflow/sessions/registry.json';
94
- if (fs.existsSync(registryPath)) {
95
- try {
96
- const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
97
- const sessionCount = Object.keys(registry.sessions || {}).length;
98
- if (sessionCount > 1) {
99
- activeSections.push('multi-session');
100
- }
101
- } catch {
102
- // Silently ignore registry read errors
103
- }
104
- }
46
+ // Claude Code's Bash tool truncates around 30K chars
47
+ const DISPLAY_LIMIT = 29200;
105
48
 
106
- return { activeSections, params };
107
- }
49
+ // =============================================================================
50
+ // Parse Arguments
51
+ // =============================================================================
108
52
 
109
- // Parse arguments
110
53
  const commandName = process.argv[2];
111
54
  const commandArgs = process.argv.slice(3);
112
55
  const { activeSections, params: commandParams } = parseCommandArgs(commandArgs);
113
56
 
114
- // Helper to extract command type from frontmatter
115
- function getCommandType(cmdName) {
116
- // Handle nested command paths like "research/ask" -> "research/ask.md"
117
- // The command name may contain "/" for nested commands
118
- const cmdPath = cmdName.includes('/')
119
- ? `${cmdName.substring(0, cmdName.lastIndexOf('/'))}/${cmdName.substring(cmdName.lastIndexOf('/') + 1)}.md`
120
- : `${cmdName}.md`;
121
-
122
- // Try to find the command file and read its frontmatter type
123
- const possiblePaths = [
124
- `packages/cli/src/core/commands/${cmdPath}`,
125
- `.agileflow/commands/${cmdPath}`,
126
- `.claude/commands/agileflow/${cmdPath}`,
127
- // Also try flat path for legacy commands
128
- `packages/cli/src/core/commands/${cmdName.replace(/\//g, '-')}.md`,
129
- ];
57
+ // =============================================================================
58
+ // Command Registration (for PreCompact context preservation)
59
+ // =============================================================================
130
60
 
131
- for (const searchPath of possiblePaths) {
132
- if (fs.existsSync(searchPath)) {
133
- try {
134
- const content = fs.readFileSync(searchPath, 'utf8');
135
- // Extract type from YAML frontmatter
136
- const match = content.match(/^---\n[\s\S]*?type:\s*(\S+)/m);
137
- if (match) {
138
- return match[1].replace(/['"]/g, ''); // Remove quotes if any
139
- }
140
- } catch {
141
- // Continue to next path
142
- }
143
- }
61
+ function registerCommand() {
62
+ if (!commandName || !isValidCommandName(commandName)) {
63
+ return;
144
64
  }
145
- return 'interactive'; // Default to interactive
146
- }
147
65
 
148
- // Register command for PreCompact context preservation
149
- if (commandName && isValidCommandName(commandName)) {
150
66
  const sessionStatePath = 'docs/09-agents/session-state.json';
151
- if (fs.existsSync(sessionStatePath)) {
152
- try {
153
- const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
154
-
155
- // Initialize active_commands array if not present
156
- if (!Array.isArray(state.active_commands)) {
157
- state.active_commands = [];
158
- }
159
-
160
- // Remove any existing entry for this command (avoid duplicates)
161
- state.active_commands = state.active_commands.filter(c => c.name !== commandName);
162
-
163
- // Get command type from frontmatter (output-only vs interactive)
164
- const commandType = getCommandType(commandName);
165
-
166
- // Add the new command with active sections for progressive disclosure
167
- state.active_commands.push({
168
- name: commandName,
169
- type: commandType, // Used by PreCompact to skip output-only commands
170
- activated_at: new Date().toISOString(),
171
- state: {},
172
- active_sections: activeSections,
173
- params: commandParams,
174
- });
175
-
176
- // Remove legacy active_command field (only use active_commands array now)
177
- if (state.active_command !== undefined) {
178
- delete state.active_command;
179
- }
180
-
181
- fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
182
- } catch (e) {
183
- // Silently continue if session state can't be updated
184
- }
185
- }
186
- }
187
-
188
- function safeRead(filePath) {
189
- try {
190
- return fs.readFileSync(filePath, 'utf8');
191
- } catch {
192
- return null;
67
+ if (!fs.existsSync(sessionStatePath)) {
68
+ return;
193
69
  }
194
- }
195
-
196
- function safeReadJSON(filePath) {
197
- // Use cached read for common JSON files
198
- const absPath = path.resolve(filePath);
199
- return readJSONCached(absPath);
200
- }
201
70
 
202
- function safeLs(dirPath) {
203
71
  try {
204
- return fs.readdirSync(dirPath);
205
- } catch {
206
- return [];
207
- }
208
- }
209
-
210
- function safeExec(cmd) {
211
- try {
212
- return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
213
- } catch {
214
- return null;
215
- }
216
- }
72
+ const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
217
73
 
218
- // =============================================================================
219
- // Context Budget Tracking (GSD Integration)
220
- // =============================================================================
221
-
222
- /**
223
- * Get current context usage percentage from Claude's session files.
224
- * Reads token counts from the active session JSONL file.
225
- *
226
- * @returns {{ percent: number, tokens: number, max: number } | null}
227
- */
228
- function getContextPercentage() {
229
- try {
230
- const homeDir = os.homedir();
231
- const cwd = process.cwd();
232
-
233
- // Convert current dir to Claude's session file path format
234
- // e.g., /home/coder/AgileFlow -> home-coder-AgileFlow
235
- const projectDir = cwd.replace(homeDir, '~').replace('~', homeDir).replace(/\//g, '-').replace(/^-/, '');
236
- const sessionDir = path.join(homeDir, '.claude', 'projects', `-${projectDir}`);
237
-
238
- if (!fs.existsSync(sessionDir)) {
239
- return null;
74
+ // Initialize active_commands array if not present
75
+ if (!Array.isArray(state.active_commands)) {
76
+ state.active_commands = [];
240
77
  }
241
78
 
242
- // Find most recent .jsonl session file
243
- const files = fs.readdirSync(sessionDir)
244
- .filter(f => f.endsWith('.jsonl'))
245
- .map(f => ({
246
- name: f,
247
- mtime: fs.statSync(path.join(sessionDir, f)).mtime.getTime(),
248
- }))
249
- .sort((a, b) => b.mtime - a.mtime);
79
+ // Remove any existing entry for this command (avoid duplicates)
80
+ state.active_commands = state.active_commands.filter(c => c.name !== commandName);
250
81
 
251
- if (files.length === 0) {
252
- return null;
253
- }
82
+ // Get command type from frontmatter
83
+ const commandType = getCommandType(commandName);
254
84
 
255
- const sessionFile = path.join(sessionDir, files[0].name);
256
- const content = fs.readFileSync(sessionFile, 'utf8');
257
- const lines = content.trim().split('\n').slice(-20); // Last 20 lines
258
-
259
- // Find latest usage entry
260
- let latestTokens = 0;
261
- for (const line of lines.reverse()) {
262
- try {
263
- const entry = JSON.parse(line);
264
- if (entry?.message?.usage) {
265
- const usage = entry.message.usage;
266
- latestTokens = (usage.input_tokens || 0) + (usage.cache_read_input_tokens || 0);
267
- if (latestTokens > 0) break;
268
- }
269
- } catch {
270
- // Skip malformed lines
271
- }
272
- }
85
+ // Add the new command with active sections
86
+ state.active_commands.push({
87
+ name: commandName,
88
+ type: commandType,
89
+ activated_at: new Date().toISOString(),
90
+ state: {},
91
+ active_sections: activeSections,
92
+ params: commandParams,
93
+ });
273
94
 
274
- if (latestTokens === 0) {
275
- return null;
95
+ // Remove legacy active_command field
96
+ if (state.active_command !== undefined) {
97
+ delete state.active_command;
276
98
  }
277
99
 
278
- // Default to 200K context for modern Claude models
279
- const maxContext = 200000;
280
- const percent = Math.min(100, Math.round((latestTokens * 100) / maxContext));
281
-
282
- return { percent, tokens: latestTokens, max: maxContext };
100
+ fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
283
101
  } catch {
284
- return null;
102
+ // Silently continue if session state can't be updated
285
103
  }
286
104
  }
287
105
 
288
- /**
289
- * Generate context budget warning box if usage exceeds threshold.
290
- * Based on GSD research: 50% is where quality starts degrading.
291
- *
292
- * @param {number} percent - Context usage percentage
293
- * @returns {string} Warning box or empty string
294
- */
295
- function generateContextWarning(percent) {
296
- if (percent < 50) {
297
- return ''; // No warning needed
298
- }
299
-
300
- const width = 60;
301
- const topLine = `┏${'━'.repeat(width - 2)}┓`;
302
- const bottomLine = `┗${'━'.repeat(width - 2)}┛`;
303
-
304
- let color, icon, message, suggestion;
305
-
306
- if (percent >= 70) {
307
- // Critical: Dumb Zone
308
- color = C.coral;
309
- icon = '🔴';
310
- message = `Context usage: ${percent}% (in degradation zone)`;
311
- suggestion = 'Strongly recommend: compact conversation or delegate to sub-agent';
312
- } else {
313
- // Warning: Approaching limit (50-69%)
314
- color = C.amber;
315
- icon = '⚠️';
316
- message = `Context usage: ${percent}% (approaching 50% threshold)`;
317
- suggestion = 'Consider: delegate to sub-agent or compact conversation';
318
- }
319
-
320
- // Pad messages to fit width
321
- const msgPadded = ` ${icon} ${message}`.padEnd(width - 3) + '┃';
322
- const sugPadded = ` → ${suggestion}`.padEnd(width - 3) + '┃';
323
-
324
- return [
325
- `${color}${C.bold}${topLine}${C.reset}`,
326
- `${color}${C.bold}┃${msgPadded}${C.reset}`,
327
- `${color}${C.bold}┃${sugPadded}${C.reset}`,
328
- `${color}${C.bold}${bottomLine}${C.reset}`,
329
- '',
330
- ].join('\n');
331
- }
332
-
333
106
  // =============================================================================
334
- // Lazy Evaluation Configuration (US-0093)
107
+ // Query Mode (US-0127)
335
108
  // =============================================================================
336
109
 
337
- /**
338
- * Commands that need full research notes content
339
- */
340
- const RESEARCH_COMMANDS = ['research', 'ideate', 'mentor', 'rpi'];
341
-
342
- /**
343
- * Determine which sections need to be loaded based on command and environment.
344
- *
345
- * @param {string} cmdName - Command name being executed
346
- * @param {Object} lazyConfig - Lazy context configuration from metadata
347
- * @param {boolean} isMultiSession - Whether multiple sessions are detected
348
- * @returns {Object} Sections to load { researchContent, sessionClaims, fileOverlaps }
349
- */
350
- function determineSectionsToLoad(cmdName, lazyConfig, isMultiSession) {
351
- // If lazy loading is disabled, load everything
352
- if (!lazyConfig?.enabled) {
353
- return {
354
- researchContent: true,
355
- sessionClaims: true,
356
- fileOverlaps: true,
357
- };
358
- }
359
-
360
- // Research notes: load for research-related commands or if 'always'
361
- const needsResearch =
362
- lazyConfig.researchNotes === 'always' ||
363
- (lazyConfig.researchNotes === 'conditional' && RESEARCH_COMMANDS.includes(cmdName));
364
-
365
- // Session claims: load if multi-session environment or if 'always'
366
- const needsClaims =
367
- lazyConfig.sessionClaims === 'always' ||
368
- (lazyConfig.sessionClaims === 'conditional' && isMultiSession);
369
-
370
- // File overlaps: load if multi-session environment or if 'always'
371
- const needsOverlaps =
372
- lazyConfig.fileOverlaps === 'always' ||
373
- (lazyConfig.fileOverlaps === 'conditional' && isMultiSession);
374
-
375
- return {
376
- researchContent: needsResearch,
377
- sessionClaims: needsClaims,
378
- fileOverlaps: needsOverlaps,
379
- };
380
- }
381
-
382
- // =============================================================================
383
- // Async I/O Functions for Parallel Pre-fetching
384
- // =============================================================================
385
-
386
- async function safeReadAsync(filePath) {
387
- try {
388
- return await fsPromises.readFile(filePath, 'utf8');
389
- } catch {
390
- return null;
391
- }
392
- }
393
-
394
- async function safeReadJSONAsync(filePath) {
395
- try {
396
- const content = await fsPromises.readFile(filePath, 'utf8');
397
- return JSON.parse(content);
398
- } catch {
399
- return null;
400
- }
401
- }
402
-
403
- async function safeLsAsync(dirPath) {
404
- try {
405
- return await fsPromises.readdir(dirPath);
406
- } catch {
407
- return [];
408
- }
409
- }
410
-
411
- /**
412
- * Execute a command asynchronously using child_process.exec
413
- * @param {string} cmd - Command to execute
414
- * @returns {Promise<string|null>} Command output or null on error
415
- */
416
- async function safeExecAsync(cmd) {
417
- const { exec } = require('child_process');
418
- return new Promise(resolve => {
419
- exec(cmd, { encoding: 'utf8' }, (error, stdout) => {
420
- if (error) {
421
- resolve(null);
422
- } else {
423
- resolve(stdout.trim());
424
- }
425
- });
426
- });
427
- }
428
-
429
- /**
430
- * Pre-fetch all required data in parallel for optimal performance.
431
- * This dramatically reduces I/O wait time by overlapping file reads and git commands.
432
- *
433
- * Lazy loading (US-0093): Only fetches content based on sectionsToLoad parameter.
434
- *
435
- * @param {Object} options - Options for prefetching
436
- * @param {Object} options.sectionsToLoad - Which sections need full content
437
- * @returns {Object} Pre-fetched data for content generation
438
- */
439
- async function prefetchAllData(options = {}) {
440
- const sectionsToLoad = options.sectionsToLoad || {
441
- researchContent: true,
442
- sessionClaims: true,
443
- fileOverlaps: true,
444
- };
445
- // Define all files to read
446
- const jsonFiles = {
447
- metadata: 'docs/00-meta/agileflow-metadata.json',
448
- statusJson: 'docs/09-agents/status.json',
449
- sessionState: 'docs/09-agents/session-state.json',
450
- };
451
-
452
- const textFiles = {
453
- busLog: 'docs/09-agents/bus/log.jsonl',
454
- claudeMd: 'CLAUDE.md',
455
- readmeMd: 'README.md',
456
- archReadme: 'docs/04-architecture/README.md',
457
- practicesReadme: 'docs/02-practices/README.md',
458
- roadmap: 'docs/08-project/roadmap.md',
459
- };
460
-
461
- const directories = {
462
- docs: 'docs',
463
- research: 'docs/10-research',
464
- epics: 'docs/05-epics',
465
- };
466
-
467
- // Git commands to run in parallel
468
- const gitCommands = {
469
- branch: 'git branch --show-current',
470
- commitShort: 'git log -1 --format="%h"',
471
- commitMsg: 'git log -1 --format="%s"',
472
- commitFull: 'git log -1 --format="%h %s"',
473
- status: 'git status --short',
474
- };
475
-
476
- // Create all promises for parallel execution
477
- const jsonPromises = Object.entries(jsonFiles).map(async ([key, filePath]) => {
478
- const data = await safeReadJSONAsync(filePath);
479
- return [key, data];
480
- });
481
-
482
- const textPromises = Object.entries(textFiles).map(async ([key, filePath]) => {
483
- const data = await safeReadAsync(filePath);
484
- return [key, data];
485
- });
486
-
487
- const dirPromises = Object.entries(directories).map(async ([key, dirPath]) => {
488
- const files = await safeLsAsync(dirPath);
489
- return [key, files];
490
- });
491
-
492
- const gitPromises = Object.entries(gitCommands).map(async ([key, cmd]) => {
493
- const data = await safeExecAsync(cmd);
494
- return [key, data];
495
- });
496
-
497
- // Execute all I/O operations in parallel
498
- const [jsonResults, textResults, dirResults, gitResults] = await Promise.all([
499
- Promise.all(jsonPromises),
500
- Promise.all(textPromises),
501
- Promise.all(dirPromises),
502
- Promise.all(gitPromises),
503
- ]);
504
-
505
- // Convert arrays back to objects
506
- const json = Object.fromEntries(jsonResults);
507
- const text = Object.fromEntries(textResults);
508
- const dirs = Object.fromEntries(dirResults);
509
- const git = Object.fromEntries(gitResults);
510
-
511
- // Determine most recent research file
512
- const researchFiles = dirs.research
513
- .filter(f => f.endsWith('.md') && f !== 'README.md')
514
- .sort()
515
- .reverse();
516
-
517
- // Lazy loading (US-0093): Only fetch research content if needed
518
- let mostRecentResearch = null;
519
- if (sectionsToLoad.researchContent && researchFiles.length > 0) {
520
- mostRecentResearch = await safeReadAsync(path.join('docs/10-research', researchFiles[0]));
521
- }
522
-
523
- return {
524
- json,
525
- text,
526
- dirs,
527
- git,
528
- researchFiles,
529
- mostRecentResearch,
530
- sectionsToLoad, // Pass through for content generation
531
- };
532
- }
533
-
534
- // ============================================
535
- // GENERATE SUMMARY (calculated first for positioning)
536
- // ============================================
537
-
538
- /**
539
- * Generate summary content using pre-fetched data.
540
- * @param {Object} prefetched - Pre-fetched data from prefetchAllData()
541
- * @returns {string} Summary content
542
- */
543
- function generateSummary(prefetched = null) {
544
- // Box drawing characters
545
- const box = {
546
- tl: '╭',
547
- tr: '╮',
548
- bl: '╰',
549
- br: '╯',
550
- h: '─',
551
- v: '│',
552
- lT: '├',
553
- rT: '┤',
554
- tT: '┬',
555
- bT: '┴',
556
- cross: '┼',
557
- };
558
-
559
- const W = 58; // Total inner width (matches welcome script)
560
- const L = 20; // Left column width
561
- const R = W - 24; // Right column width (34 chars) - matches welcome
562
-
563
- // Pad string to length, accounting for ANSI codes
564
- function pad(str, len) {
565
- const stripped = str.replace(/\x1b\[[0-9;]*m/g, '');
566
- const diff = len - stripped.length;
567
- if (diff <= 0) return str;
568
- return str + ' '.repeat(diff);
569
- }
570
-
571
- // Truncate string to max length, respecting ANSI codes
572
- function truncate(str, maxLen, suffix = '..') {
573
- const stripped = str.replace(/\x1b\[[0-9;]*m/g, '');
574
- if (stripped.length <= maxLen) return str;
575
-
576
- const targetLen = maxLen - suffix.length;
577
- let visibleCount = 0;
578
- let cutIndex = 0;
579
- let inEscape = false;
580
-
581
- for (let i = 0; i < str.length; i++) {
582
- if (str[i] === '\x1b') {
583
- inEscape = true;
584
- } else if (inEscape && str[i] === 'm') {
585
- inEscape = false;
586
- } else if (!inEscape) {
587
- visibleCount++;
588
- if (visibleCount >= targetLen) {
589
- cutIndex = i + 1;
590
- break;
591
- }
592
- }
593
- }
594
- return str.substring(0, cutIndex) + suffix;
595
- }
596
-
597
- // Create a row with auto-truncation
598
- function row(left, right, leftColor = '', rightColor = '') {
599
- const leftStr = `${leftColor}${left}${leftColor ? C.reset : ''}`;
600
- const rightTrunc = truncate(right, R);
601
- const rightStr = `${rightColor}${rightTrunc}${rightColor ? C.reset : ''}`;
602
- return `${C.dim}${box.v}${C.reset} ${pad(leftStr, L)} ${C.dim}${box.v}${C.reset} ${pad(rightStr, R)} ${C.dim}${box.v}${C.reset}\n`;
603
- }
604
-
605
- // All borders use same width formula: 22 dashes + separator + 36 dashes = 61 total chars
606
- const divider = () =>
607
- `${C.dim}${box.lT}${box.h.repeat(L + 2)}${box.cross}${box.h.repeat(W - L - 2)}${box.rT}${C.reset}\n`;
608
- const headerTopBorder = `${C.dim}${box.tl}${box.h.repeat(L + 2)}${box.tT}${box.h.repeat(W - L - 2)}${box.tr}${C.reset}\n`;
609
- const headerDivider = `${C.dim}${box.lT}${box.h.repeat(L + 2)}${box.tT}${box.h.repeat(W - L - 2)}${box.rT}${C.reset}\n`;
610
- const bottomBorder = `${C.dim}${box.bl}${box.h.repeat(L + 2)}${box.bT}${box.h.repeat(W - L - 2)}${box.br}${C.reset}\n`;
611
-
612
- // Gather data - use prefetched when available, fallback to sync reads
613
- const branch = prefetched?.git?.branch ?? safeExec('git branch --show-current') ?? 'unknown';
614
- const lastCommitShort =
615
- prefetched?.git?.commitShort ?? safeExec('git log -1 --format="%h"') ?? '?';
616
- const lastCommitMsg =
617
- prefetched?.git?.commitMsg ?? safeExec('git log -1 --format="%s"') ?? 'no commits';
618
- const statusLines = (prefetched?.git?.status ?? safeExec('git status --short') ?? '')
619
- .split('\n')
620
- .filter(Boolean);
621
- const statusJson = prefetched?.json?.statusJson ?? safeReadJSON('docs/09-agents/status.json');
622
- const sessionState =
623
- prefetched?.json?.sessionState ?? safeReadJSON('docs/09-agents/session-state.json');
624
- const researchFiles =
625
- prefetched?.researchFiles ??
626
- safeLs('docs/10-research')
627
- .filter(f => f.endsWith('.md') && f !== 'README.md')
628
- .sort()
629
- .reverse();
630
- const epicFiles =
631
- prefetched?.dirs?.epics?.filter(f => f.endsWith('.md') && f !== 'README.md') ??
632
- safeLs('docs/05-epics').filter(f => f.endsWith('.md') && f !== 'README.md');
633
-
634
- // Count stories by status
635
- const byStatus = {};
636
- const readyStories = [];
637
- if (statusJson && statusJson.stories) {
638
- Object.entries(statusJson.stories).forEach(([id, story]) => {
639
- const s = story.status || 'unknown';
640
- byStatus[s] = (byStatus[s] || 0) + 1;
641
- if (s === 'ready') readyStories.push(id);
642
- });
643
- }
644
-
645
- // Session info
646
- let sessionDuration = null;
647
- let currentStory = null;
648
- if (sessionState && sessionState.current_session && sessionState.current_session.started_at) {
649
- const started = new Date(sessionState.current_session.started_at);
650
- sessionDuration = Math.round((Date.now() - started.getTime()) / 60000);
651
- currentStory = sessionState.current_session.current_story;
652
- }
653
-
654
- // Build table
655
- let summary = '\n';
656
- summary += headerTopBorder;
657
-
658
- // Header row (full width, no column divider)
659
- const title = commandName ? `Context [${commandName}]` : 'Context Summary';
660
- const branchColor =
661
- branch === 'main' ? C.mintGreen : branch.startsWith('fix') ? C.coral : C.skyBlue;
662
- const maxBranchLen = 20;
663
- const branchDisplay =
664
- branch.length > maxBranchLen ? branch.substring(0, maxBranchLen - 2) + '..' : branch;
665
- const header = `${C.brand}${C.bold}${title}${C.reset} ${branchColor}${branchDisplay}${C.reset} ${C.dim}(${lastCommitShort})${C.reset}`;
666
- summary += `${C.dim}${box.v}${C.reset} ${pad(header, W - 1)} ${C.dim}${box.v}${C.reset}\n`;
667
-
668
- summary += headerDivider;
669
-
670
- // Story counts with vibrant 256-color palette
671
- summary += row(
672
- 'In Progress',
673
- byStatus['in-progress'] ? `${byStatus['in-progress']}` : '0',
674
- C.peach,
675
- byStatus['in-progress'] ? C.peach : C.dim
676
- );
677
- summary += row(
678
- 'Blocked',
679
- byStatus['blocked'] ? `${byStatus['blocked']}` : '0',
680
- C.coral,
681
- byStatus['blocked'] ? C.coral : C.dim
682
- );
683
- summary += row(
684
- 'Ready',
685
- byStatus['ready'] ? `${byStatus['ready']}` : '0',
686
- C.skyBlue,
687
- byStatus['ready'] ? C.skyBlue : C.dim
688
- );
689
- const completedColor = `${C.bold}${C.mintGreen}`;
690
- summary += row(
691
- 'Completed',
692
- byStatus['done'] ? `${byStatus['done']}` : '0',
693
- completedColor,
694
- byStatus['done'] ? completedColor : C.dim
695
- );
696
-
697
- summary += divider();
698
-
699
- // Git status (using vibrant 256-color palette)
700
- const uncommittedStatus =
701
- statusLines.length > 0 ? `${statusLines.length} uncommitted` : '✓ clean';
702
- summary += row('Git', uncommittedStatus, C.blue, statusLines.length > 0 ? C.peach : C.mintGreen);
703
-
704
- // Session
705
- const sessionText = sessionDuration !== null ? `${sessionDuration} min active` : 'no session';
706
- summary += row('Session', sessionText, C.blue, sessionDuration !== null ? C.lightGreen : C.dim);
707
-
708
- // Current story
709
- const storyText = currentStory ? currentStory : 'none';
710
- summary += row('Working on', storyText, C.blue, currentStory ? C.lightYellow : C.dim);
711
-
712
- // Ready stories (if any)
713
- if (readyStories.length > 0) {
714
- summary += row('⭐ Up Next', readyStories.slice(0, 3).join(', '), C.skyBlue, C.skyBlue);
715
- }
716
-
717
- // Progressive disclosure: Show active sections
718
- if (activeSections.length > 0) {
719
- summary += divider();
720
- const sectionList = activeSections.join(', ');
721
- summary += row('📖 Sections', sectionList, C.cyan, C.mintGreen);
722
- }
723
-
724
- summary += divider();
725
-
726
- // Key files (using vibrant 256-color palette)
727
- const keyFileChecks = [
728
- { path: 'CLAUDE.md', label: 'CLAUDE' },
729
- { path: 'README.md', label: 'README' },
730
- { path: 'docs/04-architecture/README.md', label: 'arch' },
731
- { path: 'docs/02-practices/README.md', label: 'practices' },
732
- ];
733
- const keyFileStatus = keyFileChecks
734
- .map(f => {
735
- const exists = fs.existsSync(f.path);
736
- return exists ? `${C.mintGreen}✓${C.reset}${f.label}` : `${C.dim}○${f.label}${C.reset}`;
737
- })
738
- .join(' ');
739
- summary += row('Key files', keyFileStatus, C.lavender, '');
740
-
741
- // Research
742
- const researchText = researchFiles.length > 0 ? `${researchFiles.length} notes` : 'none';
743
- summary += row(
744
- 'Research',
745
- researchText,
746
- C.lavender,
747
- researchFiles.length > 0 ? C.skyBlue : C.dim
748
- );
749
-
750
- // Epics
751
- const epicText = epicFiles.length > 0 ? `${epicFiles.length} epics` : 'none';
752
- summary += row('Epics', epicText, C.lavender, epicFiles.length > 0 ? C.skyBlue : C.dim);
753
-
754
- summary += divider();
755
-
756
- // Last commit (using vibrant 256-color palette)
757
- summary += row(
758
- 'Last commit',
759
- `${C.peach}${lastCommitShort}${C.reset} ${lastCommitMsg}`,
760
- C.dim,
761
- ''
762
- );
763
-
764
- summary += bottomBorder;
765
-
766
- return summary;
767
- }
768
-
769
- // ============================================
770
- // GENERATE FULL CONTENT
771
- // ============================================
772
-
773
- /**
774
- * Generate full content using pre-fetched data.
775
- * @param {Object} prefetched - Pre-fetched data from prefetchAllData()
776
- * @returns {string} Full content
777
- */
778
- function generateFullContent(prefetched = null) {
779
- let content = '';
780
-
781
- const title = commandName ? `AgileFlow Context [${commandName}]` : 'AgileFlow Context';
782
- content += `${C.lavender}${C.bold}${title}${C.reset}\n`;
783
- content += `${C.dim}Generated: ${new Date().toISOString()}${C.reset}\n`;
784
-
785
- // 0.5 SESSION CONTEXT BANNER (FIRST - before everything else)
786
- // This is critical for multi-session awareness - agents need to know which session they're in
787
- const sessionManagerPath = path.join(__dirname, 'session-manager.js');
788
- const altSessionManagerPath = '.agileflow/scripts/session-manager.js';
789
-
790
- if (fs.existsSync(sessionManagerPath) || fs.existsSync(altSessionManagerPath)) {
791
- const managerPath = fs.existsSync(sessionManagerPath)
792
- ? sessionManagerPath
793
- : altSessionManagerPath;
794
- const sessionStatus = safeExec(`node "${managerPath}" status`);
795
-
796
- if (sessionStatus) {
797
- try {
798
- const statusData = JSON.parse(sessionStatus);
799
- if (statusData.current) {
800
- const session = statusData.current;
801
- const isMain = session.is_main === true;
802
- const sessionName = session.nickname
803
- ? `Session ${session.id} "${session.nickname}"`
804
- : `Session ${session.id}`;
805
-
806
- content += `\n${C.teal}${C.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}\n`;
807
- content += `${C.teal}${C.bold}📍 SESSION CONTEXT${C.reset}\n`;
808
- content += `${C.teal}${C.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}\n`;
809
-
810
- if (isMain) {
811
- content += `${C.mintGreen}${C.bold}${sessionName}${C.reset} ${C.dim}(main project)${C.reset}\n`;
812
- } else {
813
- content += `${C.peach}${C.bold}🔀 ${sessionName}${C.reset} ${C.dim}(worktree)${C.reset}\n`;
814
- content += `Branch: ${C.skyBlue}${session.branch || 'unknown'}${C.reset}\n`;
815
- content += `${C.dim}Path: ${session.path || process.cwd()}${C.reset}\n`;
816
- }
817
-
818
- // Show other active sessions prominently
819
- if (statusData.otherActive > 0) {
820
- content += `${C.amber}⚠️ ${statusData.otherActive} other active session(s)${C.reset} - check story claims below\n`;
821
- }
822
-
823
- content += `${C.teal}${C.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}\n\n`;
824
- }
825
- } catch (e) {
826
- // Silently ignore session parse errors - will still show detailed session context later
827
- }
828
- }
829
- }
830
-
831
- // 0.7 INTERACTION MODE (AskUserQuestion) - EARLY for visibility
832
- // This MUST appear before other content to ensure Claude sees it
833
- const earlyMetadata =
834
- prefetched?.json?.metadata ?? safeReadJSON('docs/00-meta/agileflow-metadata.json');
835
- const askUserQuestionConfig = earlyMetadata?.features?.askUserQuestion;
836
-
837
- if (askUserQuestionConfig?.enabled) {
838
- content += `${C.coral}${C.bold}┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓${C.reset}\n`;
839
- content += `${C.coral}${C.bold}┃ 🔔 MANDATORY: AskUserQuestion After EVERY Response ┃${C.reset}\n`;
840
- content += `${C.coral}${C.bold}┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛${C.reset}\n`;
841
- content += `${C.bold}After completing ANY task${C.reset} (implementation, fix, etc.):\n`;
842
- content += `${C.mintGreen}→ ALWAYS${C.reset} call ${C.skyBlue}AskUserQuestion${C.reset} tool to offer next steps\n`;
843
- content += `${C.coral}→ NEVER${C.reset} end with text like "Done!" or "What's next?"\n\n`;
844
- content += `${C.dim}Balance: Use at natural pause points. Don't ask permission for routine work.${C.reset}\n\n`;
845
- }
846
-
847
- // 0.6 CONTEXT BUDGET WARNING (GSD Integration)
848
- // Show warning when context usage approaches 50% threshold
849
- const contextUsage = getContextPercentage();
850
- if (contextUsage && contextUsage.percent >= 50) {
851
- content += generateContextWarning(contextUsage.percent);
852
- }
853
-
854
- // 0. PROGRESSIVE DISCLOSURE (section activation)
855
- if (activeSections.length > 0) {
856
- content += `\n${C.cyan}${C.bold}═══ 📖 Progressive Disclosure: Active Sections ═══${C.reset}\n`;
857
- content += `${C.dim}The following sections are activated based on command parameters.${C.reset}\n`;
858
- content += `${C.dim}Look for <!-- SECTION: name --> markers in the command file.${C.reset}\n\n`;
859
-
860
- activeSections.forEach(section => {
861
- content += ` ${C.mintGreen}✓${C.reset} ${C.bold}${section}${C.reset}\n`;
862
- });
863
-
864
- // Map sections to their triggers for context
865
- const sectionDescriptions = {
866
- 'loop-mode': 'Autonomous epic execution (MODE=loop)',
867
- 'multi-session': 'Multi-session coordination detected',
868
- 'visual-e2e': 'Visual screenshot verification (VISUAL=true)',
869
- delegation: 'Expert spawning patterns (load when spawning)',
870
- stuck: 'Research prompt guidance (load after 2 failures)',
871
- 'plan-mode': 'Planning workflow details (load when entering plan mode)',
872
- tools: 'Tool usage guidance (load when needed)',
873
- };
874
-
875
- content += `\n${C.dim}Section meanings:${C.reset}\n`;
876
- activeSections.forEach(section => {
877
- const desc = sectionDescriptions[section] || 'Conditional content';
878
- content += ` ${C.dim}• ${section}: ${desc}${C.reset}\n`;
879
- });
880
- content += '\n';
881
- }
882
-
883
- // 1. GIT STATUS (using vibrant 256-color palette)
884
- content += `\n${C.skyBlue}${C.bold}═══ Git Status ═══${C.reset}\n`;
885
- const branch = prefetched?.git?.branch ?? safeExec('git branch --show-current') ?? 'unknown';
886
- const status = prefetched?.git?.status ?? safeExec('git status --short') ?? '';
887
- const statusLines = status.split('\n').filter(Boolean);
888
- const lastCommit =
889
- prefetched?.git?.commitFull ?? safeExec('git log -1 --format="%h %s"') ?? 'no commits';
890
-
891
- content += `Branch: ${C.mintGreen}${branch}${C.reset}\n`;
892
- content += `Last commit: ${C.dim}${lastCommit}${C.reset}\n`;
893
- if (statusLines.length > 0) {
894
- content += `Uncommitted: ${C.peach}${statusLines.length} file(s)${C.reset}\n`;
895
- statusLines.slice(0, 10).forEach(line => (content += ` ${C.dim}${line}${C.reset}\n`));
896
- if (statusLines.length > 10)
897
- content += ` ${C.dim}... and ${statusLines.length - 10} more${C.reset}\n`;
898
- } else {
899
- content += `Uncommitted: ${C.mintGreen}clean${C.reset}\n`;
900
- }
901
-
902
- // 2. STATUS.JSON - Full Content (using vibrant 256-color palette)
903
- content += `\n${C.skyBlue}${C.bold}═══ Status.json (Full Content) ═══${C.reset}\n`;
904
- const statusJsonPath = 'docs/09-agents/status.json';
905
- const statusJson = prefetched?.json?.statusJson ?? safeReadJSON(statusJsonPath);
906
-
907
- if (statusJson) {
908
- content += `${C.dim}${'─'.repeat(50)}${C.reset}\n`;
909
- content +=
910
- JSON.stringify(statusJson, null, 2)
911
- .split('\n')
912
- .map(l => ` ${l}`)
913
- .join('\n') + '\n';
914
- content += `${C.dim}${'─'.repeat(50)}${C.reset}\n`;
915
- } else {
916
- content += `${C.dim}No status.json found${C.reset}\n`;
917
- }
918
-
919
- // 3. SESSION STATE (using vibrant 256-color palette)
920
- content += `\n${C.skyBlue}${C.bold}═══ Session State ═══${C.reset}\n`;
921
- const sessionState =
922
- prefetched?.json?.sessionState ?? safeReadJSON('docs/09-agents/session-state.json');
923
- if (sessionState) {
924
- const current = sessionState.current_session;
925
- if (current && current.started_at) {
926
- const started = new Date(current.started_at);
927
- const duration = Math.round((Date.now() - started.getTime()) / 60000);
928
- content += `Active session: ${C.lightGreen}${duration} min${C.reset}\n`;
929
- if (current.current_story) {
930
- content += `Working on: ${C.lightYellow}${current.current_story}${C.reset}\n`;
931
- }
932
- } else {
933
- content += `${C.dim}No active session${C.reset}\n`;
934
- }
935
- // Show all active commands (array)
936
- if (Array.isArray(sessionState.active_commands) && sessionState.active_commands.length > 0) {
937
- const cmdNames = sessionState.active_commands.map(c => c.name).join(', ');
938
- content += `Active commands: ${C.skyBlue}${cmdNames}${C.reset}\n`;
939
- } else if (sessionState.active_command) {
940
- // Backwards compatibility for old format
941
- content += `Active command: ${C.skyBlue}${sessionState.active_command.name}${C.reset}\n`;
942
- }
943
-
944
- // Show batch loop status if active
945
- const batchLoop = sessionState.batch_loop;
946
- if (batchLoop && batchLoop.enabled) {
947
- content += `\n${C.skyBlue}${C.bold}── Batch Loop Active ──${C.reset}\n`;
948
- content += `Pattern: ${C.cyan}${batchLoop.pattern}${C.reset}\n`;
949
- content += `Action: ${C.cyan}${batchLoop.action}${C.reset}\n`;
950
- content += `Current: ${C.lightYellow}${batchLoop.current_item || 'none'}${C.reset}\n`;
951
- const summary = batchLoop.summary || {};
952
- content += `Progress: ${C.lightGreen}${summary.completed || 0}${C.reset}/${summary.total || 0} `;
953
- content += `(${C.lightYellow}${summary.in_progress || 0}${C.reset} in progress)\n`;
954
- content += `Iteration: ${batchLoop.iteration || 0}/${batchLoop.max_iterations || 50}\n`;
955
- }
956
- } else {
957
- content += `${C.dim}No session-state.json found${C.reset}\n`;
958
- }
959
-
960
- // 4. SESSION CONTEXT (details - banner shown above)
961
- // Note: Prominent SESSION CONTEXT banner is shown at the top of output
962
- // This section provides additional details for non-main sessions
963
- const sessionMgrPath = path.join(__dirname, 'session-manager.js');
964
- const altSessionMgrPath = '.agileflow/scripts/session-manager.js';
965
-
966
- if (fs.existsSync(sessionMgrPath) || fs.existsSync(altSessionMgrPath)) {
967
- const mgrPath = fs.existsSync(sessionMgrPath) ? sessionMgrPath : altSessionMgrPath;
968
- const sessionStatusStr = safeExec(`node "${mgrPath}" status`);
969
-
970
- if (sessionStatusStr) {
971
- try {
972
- const statusData = JSON.parse(sessionStatusStr);
973
- if (statusData.current && !statusData.current.is_main) {
974
- // Only show additional details for non-main sessions
975
- content += `\n${C.skyBlue}${C.bold}═══ Session Details ═══${C.reset}\n`;
976
- const session = statusData.current;
977
-
978
- // Calculate relative path to main
979
- const mainPath = process.cwd().replace(/-[^/]+$/, ''); // Heuristic: strip session suffix
980
- content += `Main project: ${C.dim}${mainPath}${C.reset}\n`;
981
-
982
- // Remind about merge flow
983
- content += `${C.lavender}💡 When done: /agileflow:session:end → merge to main${C.reset}\n`;
984
- }
985
- } catch (e) {
986
- // Silently ignore - banner above has basic info
987
- }
988
- }
989
- }
990
-
991
- // 5. STORY CLAIMS (inter-session coordination)
992
- // Lazy loading (US-0093): Only load if sectionsToLoad.sessionClaims is true
993
- const shouldLoadClaims = prefetched?.sectionsToLoad?.sessionClaims !== false;
994
-
995
- if (shouldLoadClaims) {
996
- const storyClaimingPath = path.join(__dirname, 'lib', 'story-claiming.js');
997
- const altStoryClaimingPath = '.agileflow/scripts/lib/story-claiming.js';
998
-
999
- if (fs.existsSync(storyClaimingPath) || fs.existsSync(altStoryClaimingPath)) {
1000
- try {
1001
- const claimPath = fs.existsSync(storyClaimingPath)
1002
- ? storyClaimingPath
1003
- : altStoryClaimingPath;
1004
- const storyClaiming = require(claimPath);
1005
-
1006
- // Get stories claimed by other sessions
1007
- const othersResult = storyClaiming.getStoriesClaimedByOthers();
1008
- if (othersResult.ok && othersResult.stories && othersResult.stories.length > 0) {
1009
- content += `\n${C.amber}${C.bold}═══ 🔒 Claimed Stories ═══${C.reset}\n`;
1010
- content += `${C.dim}Stories locked by other sessions - pick a different one${C.reset}\n`;
1011
- othersResult.stories.forEach(story => {
1012
- const sessionDir = story.claimedBy?.path
1013
- ? path.basename(story.claimedBy.path)
1014
- : 'unknown';
1015
- content += ` ${C.coral}🔒${C.reset} ${C.lavender}${story.id}${C.reset} "${story.title}" ${C.dim}→ Session ${story.claimedBy?.session_id || '?'} (${sessionDir})${C.reset}\n`;
1016
- });
1017
- content += '\n';
1018
- }
1019
-
1020
- // Get stories claimed by THIS session
1021
- const myResult = storyClaiming.getClaimedStoriesForSession();
1022
- if (myResult.ok && myResult.stories && myResult.stories.length > 0) {
1023
- content += `\n${C.mintGreen}${C.bold}═══ ✓ Your Claimed Stories ═══${C.reset}\n`;
1024
- myResult.stories.forEach(story => {
1025
- content += ` ${C.mintGreen}✓${C.reset} ${C.lavender}${story.id}${C.reset} "${story.title}"\n`;
1026
- });
1027
- content += '\n';
1028
- }
1029
- } catch (e) {
1030
- // Story claiming not available or error - silently skip
1031
- }
1032
- }
1033
- }
1034
-
1035
- // 5b. FILE OVERLAPS (inter-session file awareness)
1036
- // Lazy loading (US-0093): Only load if sectionsToLoad.fileOverlaps is true
1037
- const shouldLoadOverlaps = prefetched?.sectionsToLoad?.fileOverlaps !== false;
1038
-
1039
- if (shouldLoadOverlaps) {
1040
- const fileTrackingPath = path.join(__dirname, 'lib', 'file-tracking.js');
1041
- const altFileTrackingPath = '.agileflow/scripts/lib/file-tracking.js';
1042
-
1043
- if (fs.existsSync(fileTrackingPath) || fs.existsSync(altFileTrackingPath)) {
1044
- try {
1045
- const trackPath = fs.existsSync(fileTrackingPath) ? fileTrackingPath : altFileTrackingPath;
1046
- const fileTracking = require(trackPath);
1047
-
1048
- // Get file overlaps with other sessions
1049
- const overlapsResult = fileTracking.getMyFileOverlaps();
1050
- if (overlapsResult.ok && overlapsResult.overlaps && overlapsResult.overlaps.length > 0) {
1051
- content += `\n${C.amber}${C.bold}═══ ⚠️ File Overlaps ═══${C.reset}\n`;
1052
- content += `${C.dim}Files also edited by other sessions - conflicts auto-resolved during merge${C.reset}\n`;
1053
- overlapsResult.overlaps.forEach(overlap => {
1054
- const sessionInfo = overlap.otherSessions
1055
- .map(s => {
1056
- const dir = path.basename(s.path);
1057
- return `Session ${s.id} (${dir})`;
1058
- })
1059
- .join(', ');
1060
- content += ` ${C.amber}⚠${C.reset} ${C.lavender}${overlap.file}${C.reset} ${C.dim}→ ${sessionInfo}${C.reset}\n`;
1061
- });
1062
- content += '\n';
1063
- }
1064
-
1065
- // Show files touched by this session
1066
- const { getCurrentSession, getSessionFiles } = fileTracking;
1067
- const currentSession = getCurrentSession();
1068
- if (currentSession) {
1069
- const filesResult = getSessionFiles(currentSession.session_id);
1070
- if (filesResult.ok && filesResult.files && filesResult.files.length > 0) {
1071
- content += `\n${C.skyBlue}${C.bold}═══ 📁 Files Touched This Session ═══${C.reset}\n`;
1072
- content += `${C.dim}${filesResult.files.length} files tracked for conflict detection${C.reset}\n`;
1073
- // Show first 5 files max
1074
- const displayFiles = filesResult.files.slice(0, 5);
1075
- displayFiles.forEach(file => {
1076
- content += ` ${C.dim}•${C.reset} ${file}\n`;
1077
- });
1078
- if (filesResult.files.length > 5) {
1079
- content += ` ${C.dim}... and ${filesResult.files.length - 5} more${C.reset}\n`;
1080
- }
1081
- content += '\n';
1082
- }
1083
- }
1084
- } catch (e) {
1085
- // File tracking not available or error - silently skip
1086
- }
1087
- }
1088
- }
1089
-
1090
- // 6. VISUAL E2E STATUS (detect from metadata or filesystem)
1091
- const metadata =
1092
- prefetched?.json?.metadata ?? safeReadJSON('docs/00-meta/agileflow-metadata.json');
1093
- const visualE2eConfig = metadata?.features?.visual_e2e;
1094
- const playwrightExists =
1095
- fs.existsSync('playwright.config.ts') || fs.existsSync('playwright.config.js');
1096
- const screenshotsExists = fs.existsSync('screenshots');
1097
- const testsE2eExists = fs.existsSync('tests/e2e');
1098
-
1099
- // Determine visual e2e status
1100
- const visualE2eEnabled = visualE2eConfig?.enabled || (playwrightExists && screenshotsExists);
1101
-
1102
- if (visualE2eEnabled) {
1103
- content += `\n${C.brand}${C.bold}═══ 📸 VISUAL E2E TESTING: ENABLED ═══${C.reset}\n`;
1104
- content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
1105
- content += `${C.mintGreen}✓ Playwright:${C.reset} ${playwrightExists ? 'configured' : 'not found'}\n`;
1106
- content += `${C.mintGreen}✓ Screenshots:${C.reset} ${screenshotsExists ? 'screenshots/' : 'not found'}\n`;
1107
- content += `${C.mintGreen}✓ E2E Tests:${C.reset} ${testsE2eExists ? 'tests/e2e/' : 'not found'}\n\n`;
1108
- content += `${C.bold}FOR UI WORK:${C.reset} Use ${C.skyBlue}VISUAL=true${C.reset} flag with babysit:\n`;
1109
- content += `${C.dim} /agileflow:babysit EPIC=EP-XXXX MODE=loop VISUAL=true${C.reset}\n\n`;
1110
- content += `${C.lavender}Screenshot Verification Workflow:${C.reset}\n`;
1111
- content += ` 1. E2E tests capture screenshots to ${C.skyBlue}screenshots/${C.reset}\n`;
1112
- content += ` 2. Review each screenshot visually (Claude reads image files)\n`;
1113
- content += ` 3. Rename verified: ${C.dim}mv file.png verified-file.png${C.reset}\n`;
1114
- content += ` 4. All screenshots must have ${C.mintGreen}verified-${C.reset} prefix before completion\n`;
1115
- content += `${C.dim}${'─'.repeat(60)}${C.reset}\n\n`;
1116
- } else {
1117
- content += `\n${C.dim}═══ 📸 VISUAL E2E TESTING: NOT CONFIGURED ═══${C.reset}\n`;
1118
- content += `${C.dim}For UI work with screenshot verification:${C.reset}\n`;
1119
- content += `${C.dim} /agileflow:configure → Visual E2E testing${C.reset}\n\n`;
1120
- }
1121
-
1122
- // DOCS STRUCTURE (using vibrant 256-color palette)
1123
- content += `\n${C.skyBlue}${C.bold}═══ Documentation ═══${C.reset}\n`;
1124
- const docsDir = 'docs';
1125
- const docFolders = (prefetched?.dirs?.docs ?? safeLs(docsDir)).filter(f => {
1126
- try {
1127
- return fs.statSync(path.join(docsDir, f)).isDirectory();
1128
- } catch {
1129
- return false;
1130
- }
1131
- });
1132
-
1133
- if (docFolders.length > 0) {
1134
- docFolders.forEach(folder => {
1135
- const folderPath = path.join(docsDir, folder);
1136
- const files = safeLs(folderPath);
1137
- const mdFiles = files.filter(f => f.endsWith('.md'));
1138
- const jsonFiles = files.filter(f => f.endsWith('.json') || f.endsWith('.jsonl'));
1139
- const info = [];
1140
- if (mdFiles.length > 0) info.push(`${mdFiles.length} md`);
1141
- if (jsonFiles.length > 0) info.push(`${jsonFiles.length} json`);
1142
- content += ` ${C.dim}${folder}/${C.reset} ${info.length > 0 ? `(${info.join(', ')})` : ''}\n`;
1143
- });
1144
- }
1145
-
1146
- // 6. RESEARCH NOTES - List + Full content of most recent (using vibrant 256-color palette)
1147
- // Lazy loading (US-0093): Full content only loaded for research-related commands
1148
- const shouldLoadResearch = prefetched?.sectionsToLoad?.researchContent !== false;
1149
- content += `\n${C.skyBlue}${C.bold}═══ Research Notes ═══${C.reset}\n`;
1150
- const researchDir = 'docs/10-research';
1151
- const researchFiles =
1152
- prefetched?.researchFiles ??
1153
- safeLs(researchDir)
1154
- .filter(f => f.endsWith('.md') && f !== 'README.md')
1155
- .sort()
1156
- .reverse();
1157
- if (researchFiles.length > 0) {
1158
- content += `${C.dim}───${C.reset} Available Research Notes\n`;
1159
- researchFiles.forEach(file => (content += ` ${C.dim}${file}${C.reset}\n`));
1160
-
1161
- const mostRecentFile = researchFiles[0];
1162
- const mostRecentPath = path.join(researchDir, mostRecentFile);
1163
- const mostRecentContent =
1164
- prefetched?.mostRecentResearch ?? (shouldLoadResearch ? safeRead(mostRecentPath) : null);
1165
-
1166
- if (mostRecentContent) {
1167
- content += `\n${C.mintGreen}📄 Most Recent: ${mostRecentFile}${C.reset}\n`;
1168
- content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
1169
- content += mostRecentContent + '\n';
1170
- content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
1171
- } else if (!shouldLoadResearch) {
1172
- content += `\n${C.dim}📄 Content deferred (lazy loading). Use /agileflow:research to access.${C.reset}\n`;
1173
- }
1174
- } else {
1175
- content += `${C.dim}No research notes${C.reset}\n`;
1176
- }
1177
-
1178
- // 7. BUS MESSAGES (using vibrant 256-color palette)
1179
- content += `\n${C.skyBlue}${C.bold}═══ Recent Agent Messages ═══${C.reset}\n`;
1180
- const busPath = 'docs/09-agents/bus/log.jsonl';
1181
- const busContent = prefetched?.text?.busLog ?? safeRead(busPath);
1182
- if (busContent) {
1183
- const lines = busContent.trim().split('\n').filter(Boolean);
1184
- const recent = lines.slice(-5);
1185
- if (recent.length > 0) {
1186
- recent.forEach(line => {
1187
- try {
1188
- const msg = JSON.parse(line);
1189
- const time = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString() : '?';
1190
- content += ` ${C.dim}[${time}]${C.reset} ${msg.from || '?'}: ${msg.type || msg.message || '?'}\n`;
1191
- } catch {
1192
- content += ` ${C.dim}${line.substring(0, 80)}...${C.reset}\n`;
1193
- }
1194
- });
1195
- } else {
1196
- content += `${C.dim}No messages${C.reset}\n`;
1197
- }
1198
- } else {
1199
- content += `${C.dim}No bus log found${C.reset}\n`;
1200
- }
1201
-
1202
- // 8. KEY FILES - Full content
1203
- content += `\n${C.cyan}${C.bold}═══ Key Context Files (Full Content) ═══${C.reset}\n`;
1204
-
1205
- // Map file paths to prefetched keys
1206
- const prefetchedKeyMap = {
1207
- 'CLAUDE.md': 'claudeMd',
1208
- 'README.md': 'readmeMd',
1209
- 'docs/04-architecture/README.md': 'archReadme',
1210
- 'docs/02-practices/README.md': 'practicesReadme',
1211
- 'docs/08-project/roadmap.md': 'roadmap',
1212
- };
1213
-
1214
- const keyFilesToRead = [
1215
- { path: 'CLAUDE.md', label: 'CLAUDE.md (Project Instructions)' },
1216
- { path: 'README.md', label: 'README.md (Project Overview)' },
1217
- { path: 'docs/04-architecture/README.md', label: 'Architecture Index' },
1218
- { path: 'docs/02-practices/README.md', label: 'Practices Index' },
1219
- { path: 'docs/08-project/roadmap.md', label: 'Roadmap' },
1220
- ];
1221
-
1222
- keyFilesToRead.forEach(({ path: filePath, label }) => {
1223
- const prefetchKey = prefetchedKeyMap[filePath];
1224
- const fileContent = prefetched?.text?.[prefetchKey] ?? safeRead(filePath);
1225
- if (fileContent) {
1226
- content += `\n${C.green}✓ ${label}${C.reset} ${C.dim}(${filePath})${C.reset}\n`;
1227
- content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
1228
- content += fileContent + '\n';
1229
- content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
1230
- } else {
1231
- content += `${C.dim}○ ${label} (not found)${C.reset}\n`;
1232
- }
1233
- });
1234
-
1235
- const settingsExists = fs.existsSync('.claude/settings.json');
1236
- content += `\n ${settingsExists ? `${C.green}✓${C.reset}` : `${C.dim}○${C.reset}`} .claude/settings.json\n`;
1237
-
1238
- // 9. EPICS FOLDER
1239
- content += `\n${C.cyan}${C.bold}═══ Epic Files ═══${C.reset}\n`;
1240
- const epicFiles =
1241
- prefetched?.dirs?.epics?.filter(f => f.endsWith('.md') && f !== 'README.md') ??
1242
- safeLs('docs/05-epics').filter(f => f.endsWith('.md') && f !== 'README.md');
1243
- if (epicFiles.length > 0) {
1244
- epicFiles.forEach(file => (content += ` ${C.dim}${file}${C.reset}\n`));
1245
- } else {
1246
- content += `${C.dim}No epic files${C.reset}\n`;
1247
- }
1248
-
1249
- // FOOTER
1250
- content += `\n${C.dim}─────────────────────────────────────────${C.reset}\n`;
1251
- content += `${C.dim}Context gathered in single execution. Claude has full context.${C.reset}\n`;
1252
-
1253
- return content;
1254
- }
1255
-
1256
- // ============================================
1257
- // QUERY MODE: Targeted codebase search (US-0127)
1258
- // ============================================
1259
-
1260
110
  /**
1261
111
  * Execute query mode using codebase index for targeted search.
1262
112
  * Falls back to full context if query returns no results.
@@ -1267,22 +117,19 @@ function generateFullContent(prefetched = null) {
1267
117
  function executeQueryMode(query) {
1268
118
  const queryScript = path.join(__dirname, 'query-codebase.js');
1269
119
 
1270
- // Check if query script exists
1271
120
  if (!fs.existsSync(queryScript)) {
1272
121
  console.error('Query mode unavailable: query-codebase.js not found');
1273
- return null; // Fall back to full context
122
+ return null;
1274
123
  }
1275
124
 
1276
125
  try {
1277
- // Execute query and capture output
1278
126
  const result = execSync(`node "${queryScript}" --query="${query}" --budget=15000`, {
1279
127
  encoding: 'utf8',
1280
- maxBuffer: 50 * 1024 * 1024, // 50MB buffer
128
+ maxBuffer: 50 * 1024 * 1024,
1281
129
  });
1282
130
 
1283
- // Check if we got results
1284
131
  if (result.includes('No files found') || result.trim() === '') {
1285
- return null; // Fall back to full context
132
+ return null;
1286
133
  }
1287
134
 
1288
135
  return {
@@ -1291,30 +138,27 @@ function executeQueryMode(query) {
1291
138
  results: result.trim(),
1292
139
  };
1293
140
  } catch (err) {
1294
- // Exit code 2 = no results, fall back to full context
1295
141
  if (err.status === 2) {
1296
- return null;
142
+ return null; // No results, fall back
1297
143
  }
1298
- // Exit code 1 = error, report but fall back
1299
144
  console.error(`Query error: ${err.message}`);
1300
145
  return null;
1301
146
  }
1302
147
  }
1303
148
 
1304
- // ============================================
1305
- // MAIN: Output with smart summary positioning
1306
- // ============================================
149
+ // =============================================================================
150
+ // Main Execution
151
+ // =============================================================================
1307
152
 
1308
- /**
1309
- * Main execution function using parallel pre-fetching for optimal performance.
1310
- */
1311
153
  async function main() {
154
+ // Register command for PreCompact
155
+ registerCommand();
156
+
1312
157
  // Check for query mode first (US-0127)
1313
158
  if (activeSections.includes('query-mode') && commandParams.QUERY) {
1314
159
  const queryResult = executeQueryMode(commandParams.QUERY);
1315
160
 
1316
161
  if (queryResult) {
1317
- // Output query results instead of full context
1318
162
  console.log(`=== QUERY MODE ===`);
1319
163
  console.log(`Query: "${queryResult.query}"`);
1320
164
  console.log(`---`);
@@ -1323,56 +167,44 @@ async function main() {
1323
167
  console.log(`[Query mode: targeted search. Run without QUERY= for full context]`);
1324
168
  return;
1325
169
  }
1326
- // Fall through to full context if query returned no results
1327
170
  console.log(`[Query "${commandParams.QUERY}" returned no results, loading full context...]`);
1328
171
  }
1329
172
 
1330
- // Check for multi-session environment before prefetching
1331
- const registryPath = '.agileflow/sessions/registry.json';
1332
- let isMultiSession = false;
1333
- if (fs.existsSync(registryPath)) {
1334
- try {
1335
- const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
1336
- const sessionCount = Object.keys(registry.sessions || {}).length;
1337
- isMultiSession = sessionCount > 1;
1338
- } catch {
1339
- // Ignore registry read errors
1340
- }
1341
- }
173
+ // Check for multi-session environment
174
+ const isMultiSession = isMultiSessionEnvironment();
1342
175
 
1343
- // Load lazy context configuration from metadata
176
+ // Load lazy context configuration
1344
177
  const metadata = safeReadJSON('docs/00-meta/agileflow-metadata.json');
1345
178
  const lazyConfig = metadata?.features?.lazyContext;
1346
179
 
1347
180
  // Determine which sections need full content (US-0093)
1348
181
  const sectionsToLoad = determineSectionsToLoad(commandName, lazyConfig, isMultiSession);
1349
182
 
1350
- // Pre-fetch all file data in parallel with lazy loading options
183
+ // Pre-fetch all data in parallel
1351
184
  const prefetched = await prefetchAllData({ sectionsToLoad });
1352
185
 
1353
- // Generate content using pre-fetched data
1354
- const summary = generateSummary(prefetched);
1355
- const fullContent = generateFullContent(prefetched);
186
+ // Generate formatted output
187
+ const formatOptions = { commandName, activeSections };
188
+ const summary = generateSummary(prefetched, formatOptions);
189
+ const fullContent = generateFullContent(prefetched, formatOptions);
1356
190
 
191
+ // Smart output positioning
1357
192
  const summaryLength = summary.length;
1358
193
  const cutoffPoint = DISPLAY_LIMIT - summaryLength;
1359
194
 
1360
195
  if (fullContent.length <= cutoffPoint) {
1361
- // Full content fits before summary - just output everything
196
+ // Full content fits before summary
1362
197
  console.log(fullContent);
1363
198
  console.log(summary);
1364
199
  } else {
1365
- // Output content up to cutoff, then summary as the LAST visible thing.
1366
- // Don't output contentAfter - it would bleed into visible area before truncation,
1367
- // and Claude only sees ~30K chars from Bash anyway.
200
+ // Output content up to cutoff, then summary as the LAST visible thing
1368
201
  const contentBefore = fullContent.substring(0, cutoffPoint);
1369
-
1370
202
  console.log(contentBefore);
1371
203
  console.log(summary);
1372
204
  }
1373
205
  }
1374
206
 
1375
- // Execute main function
207
+ // Execute
1376
208
  main().catch(err => {
1377
209
  console.error('Error gathering context:', err.message);
1378
210
  process.exit(1);