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