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.
- package/CHANGELOG.md +10 -0
- package/README.md +6 -6
- package/lib/README.md +178 -0
- package/lib/codebase-indexer.js +818 -0
- package/lib/colors.js +190 -12
- package/lib/consent.js +232 -0
- package/lib/correlation.js +277 -0
- package/lib/error-codes.js +46 -0
- package/lib/errors.js +48 -6
- package/lib/file-cache.js +182 -0
- package/lib/format-error.js +156 -0
- package/lib/path-resolver.js +155 -7
- package/lib/paths.js +212 -20
- package/lib/placeholder-registry.js +205 -0
- package/lib/registry-di.js +358 -0
- package/lib/result-schema.js +363 -0
- package/lib/result.js +210 -0
- package/lib/session-registry.js +13 -0
- package/lib/session-state-machine.js +465 -0
- package/lib/validate-commands.js +308 -0
- package/lib/validate-names.js +3 -3
- package/lib/validate.js +116 -52
- package/package.json +4 -1
- package/scripts/af +34 -0
- package/scripts/agent-loop.js +63 -9
- package/scripts/agileflow-configure.js +2 -2
- package/scripts/agileflow-welcome.js +435 -23
- package/scripts/archive-completed-stories.sh +57 -11
- package/scripts/claude-tmux.sh +102 -0
- package/scripts/damage-control-bash.js +3 -70
- package/scripts/damage-control-edit.js +3 -20
- package/scripts/damage-control-write.js +3 -20
- package/scripts/dependency-check.js +310 -0
- package/scripts/get-env.js +11 -4
- package/scripts/lib/configure-detect.js +23 -1
- package/scripts/lib/configure-features.js +43 -2
- package/scripts/lib/context-formatter.js +771 -0
- package/scripts/lib/context-loader.js +699 -0
- package/scripts/lib/damage-control-utils.js +107 -0
- package/scripts/lib/json-utils.sh +162 -0
- package/scripts/lib/state-migrator.js +353 -0
- package/scripts/lib/story-state-machine.js +437 -0
- package/scripts/obtain-context.js +118 -1048
- package/scripts/pre-push-check.sh +46 -0
- package/scripts/precompact-context.sh +36 -11
- package/scripts/query-codebase.js +538 -0
- package/scripts/ralph-loop.js +5 -5
- package/scripts/session-manager.js +220 -42
- package/scripts/spawn-parallel.js +651 -0
- package/scripts/tui/blessed/data/watcher.js +180 -0
- package/scripts/tui/blessed/index.js +244 -0
- package/scripts/tui/blessed/panels/output.js +101 -0
- package/scripts/tui/blessed/panels/sessions.js +150 -0
- package/scripts/tui/blessed/panels/trace.js +97 -0
- package/scripts/tui/blessed/ui/help.js +77 -0
- package/scripts/tui/blessed/ui/screen.js +52 -0
- package/scripts/tui/blessed/ui/statusbar.js +47 -0
- package/scripts/tui/blessed/ui/tabbar.js +99 -0
- package/scripts/tui/index.js +38 -30
- package/scripts/validators/README.md +143 -0
- package/scripts/validators/component-validator.js +239 -0
- package/scripts/validators/json-schema-validator.js +186 -0
- package/scripts/validators/markdown-validator.js +152 -0
- package/scripts/validators/migration-validator.js +129 -0
- package/scripts/validators/security-validator.js +380 -0
- package/scripts/validators/story-format-validator.js +197 -0
- package/scripts/validators/test-result-validator.js +114 -0
- package/scripts/validators/workflow-validator.js +247 -0
- package/src/core/agents/accessibility.md +6 -0
- package/src/core/agents/adr-writer.md +6 -0
- package/src/core/agents/analytics.md +6 -0
- package/src/core/agents/api.md +6 -0
- package/src/core/agents/ci.md +6 -0
- package/src/core/agents/codebase-query.md +261 -0
- package/src/core/agents/compliance.md +6 -0
- package/src/core/agents/configuration-damage-control.md +6 -0
- package/src/core/agents/configuration-visual-e2e.md +6 -0
- package/src/core/agents/database.md +10 -0
- package/src/core/agents/datamigration.md +6 -0
- package/src/core/agents/design.md +6 -0
- package/src/core/agents/devops.md +6 -0
- package/src/core/agents/documentation.md +6 -0
- package/src/core/agents/epic-planner.md +6 -0
- package/src/core/agents/integrations.md +6 -0
- package/src/core/agents/mentor.md +6 -0
- package/src/core/agents/mobile.md +6 -0
- package/src/core/agents/monitoring.md +6 -0
- package/src/core/agents/multi-expert.md +6 -0
- package/src/core/agents/performance.md +6 -0
- package/src/core/agents/product.md +6 -0
- package/src/core/agents/qa.md +6 -0
- package/src/core/agents/readme-updater.md +6 -0
- package/src/core/agents/refactor.md +6 -0
- package/src/core/agents/research.md +6 -0
- package/src/core/agents/security.md +6 -0
- package/src/core/agents/testing.md +10 -0
- package/src/core/agents/ui.md +6 -0
- package/src/core/commands/adr.md +114 -0
- package/src/core/commands/agent.md +120 -0
- package/src/core/commands/assign.md +145 -0
- package/src/core/commands/audit.md +401 -0
- package/src/core/commands/babysit.md +32 -5
- package/src/core/commands/board.md +1 -0
- package/src/core/commands/changelog.md +118 -0
- package/src/core/commands/configure.md +42 -6
- package/src/core/commands/diagnose.md +114 -0
- package/src/core/commands/epic.md +205 -1
- package/src/core/commands/handoff.md +128 -0
- package/src/core/commands/help.md +76 -0
- package/src/core/commands/metrics.md +1 -0
- package/src/core/commands/pr.md +96 -0
- package/src/core/commands/research/analyze.md +1 -0
- package/src/core/commands/research/ask.md +2 -0
- package/src/core/commands/research/import.md +1 -0
- package/src/core/commands/research/list.md +2 -0
- package/src/core/commands/research/synthesize.md +584 -0
- package/src/core/commands/research/view.md +2 -0
- package/src/core/commands/roadmap/analyze.md +400 -0
- package/src/core/commands/session/new.md +113 -6
- package/src/core/commands/session/spawn.md +197 -0
- package/src/core/commands/sprint.md +22 -0
- package/src/core/commands/status.md +200 -1
- package/src/core/commands/story/list.md +9 -9
- package/src/core/commands/story/view.md +1 -0
- package/src/core/commands/story.md +143 -4
- package/src/core/experts/codebase-query/expertise.yaml +190 -0
- package/src/core/experts/codebase-query/question.md +73 -0
- package/src/core/experts/codebase-query/self-improve.md +105 -0
- package/src/core/templates/agileflow-metadata.json +55 -2
- package/src/core/templates/plan-template.md +125 -0
- package/src/core/templates/story-lifecycle.md +213 -0
- package/src/core/templates/story-template.md +4 -0
- package/src/core/templates/tdd-test-template.js +241 -0
- package/tools/cli/commands/setup.js +86 -0
- package/tools/cli/installers/core/installer.js +94 -0
- package/tools/cli/installers/ide/_base-ide.js +20 -11
- package/tools/cli/installers/ide/codex.js +29 -47
- package/tools/cli/lib/config-manager.js +17 -2
- package/tools/cli/lib/content-transformer.js +271 -0
- package/tools/cli/lib/error-handler.js +14 -22
- package/tools/cli/lib/ide-error-factory.js +421 -0
- package/tools/cli/lib/ide-health-monitor.js +364 -0
- package/tools/cli/lib/ide-registry.js +114 -1
- package/tools/cli/lib/ui.js +14 -25
|
@@ -2,1139 +2,209 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* obtain-context.js
|
|
4
4
|
*
|
|
5
|
-
*
|
|
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
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
58
|
+
// Command Registration (for PreCompact context preservation)
|
|
170
59
|
// =============================================================================
|
|
171
60
|
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
441
|
-
|
|
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
|
-
|
|
448
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
//
|
|
606
|
-
//
|
|
106
|
+
// =============================================================================
|
|
107
|
+
// Query Mode (US-0127)
|
|
108
|
+
// =============================================================================
|
|
607
109
|
|
|
608
110
|
/**
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
*
|
|
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
|
|
614
|
-
|
|
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(
|
|
795
|
-
|
|
796
|
-
|
|
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
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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
|
-
|
|
990
|
-
|
|
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
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
-
|
|
1027
|
-
|
|
144
|
+
console.error(`Query error: ${err.message}`);
|
|
145
|
+
return null;
|
|
1028
146
|
}
|
|
147
|
+
}
|
|
1029
148
|
|
|
1030
|
-
|
|
1031
|
-
|
|
149
|
+
// =============================================================================
|
|
150
|
+
// Main Execution
|
|
151
|
+
// =============================================================================
|
|
1032
152
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
-
//
|
|
1078
|
-
|
|
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
|
|
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
|
|
183
|
+
// Pre-fetch all data in parallel
|
|
1113
184
|
const prefetched = await prefetchAllData({ sectionsToLoad });
|
|
1114
185
|
|
|
1115
|
-
// Generate
|
|
1116
|
-
const
|
|
1117
|
-
const
|
|
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
|
|
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
|
|
207
|
+
// Execute
|
|
1138
208
|
main().catch(err => {
|
|
1139
209
|
console.error('Error gathering context:', err.message);
|
|
1140
210
|
process.exit(1);
|