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