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