agileflow 2.99.8 → 3.0.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 +3 -3
- package/lib/cache-provider.js +155 -0
- package/lib/codebase-indexer.js +1 -1
- package/lib/content-sanitizer.js +1 -0
- package/lib/dashboard-protocol.js +25 -0
- package/lib/dashboard-server.js +282 -150
- package/lib/errors.js +18 -0
- package/lib/file-cache.js +1 -1
- package/lib/flag-detection.js +11 -20
- package/lib/git-operations.js +15 -33
- package/lib/merge-operations.js +40 -34
- package/lib/process-executor.js +199 -0
- package/lib/registry-cache.js +13 -47
- package/lib/skill-loader.js +206 -0
- package/lib/smart-json-file.js +2 -4
- package/package.json +1 -1
- package/scripts/agileflow-configure.js +13 -12
- package/scripts/agileflow-statusline.sh +30 -0
- package/scripts/agileflow-welcome.js +181 -212
- package/scripts/archive-completed-stories.sh +3 -0
- package/scripts/auto-self-improve.js +3 -3
- package/scripts/ci-summary.js +294 -0
- package/scripts/claude-smart.sh +85 -0
- package/scripts/claude-tmux.sh +272 -161
- package/scripts/damage-control-multi-agent.js +227 -0
- package/scripts/lib/bus-utils.js +471 -0
- package/scripts/lib/configure-detect.js +87 -10
- package/scripts/lib/configure-features.js +110 -4
- package/scripts/lib/configure-repair.js +5 -6
- package/scripts/lib/configure-utils.js +2 -3
- package/scripts/lib/context-formatter.js +87 -8
- package/scripts/lib/damage-control-utils.js +37 -3
- package/scripts/lib/file-lock.js +392 -0
- package/scripts/lib/ideation-index.js +2 -5
- package/scripts/lib/lifecycle-detector.js +123 -0
- package/scripts/lib/process-cleanup.js +55 -81
- package/scripts/lib/scale-detector.js +357 -0
- package/scripts/lib/signal-detectors.js +779 -0
- package/scripts/lib/story-state-machine.js +1 -1
- package/scripts/lib/sync-ideation-status.js +2 -3
- package/scripts/lib/task-registry.js +7 -1
- package/scripts/lib/team-events.js +357 -0
- package/scripts/messaging-bridge.js +79 -36
- package/scripts/migrate-ideation-index.js +37 -14
- package/scripts/obtain-context.js +37 -19
- package/scripts/precompact-context.sh +3 -0
- package/scripts/ralph-loop.js +3 -4
- package/scripts/smart-detect.js +390 -0
- package/scripts/team-manager.js +174 -30
- package/src/core/commands/audit.md +13 -11
- package/src/core/commands/babysit.md +162 -115
- package/src/core/commands/changelog.md +21 -4
- package/src/core/commands/configure.md +141 -21
- package/src/core/commands/debt.md +12 -2
- package/src/core/commands/feedback.md +7 -6
- package/src/core/commands/ideate/history.md +1 -1
- package/src/core/commands/ideate/new.md +5 -5
- package/src/core/commands/logic/audit.md +2 -2
- package/src/core/commands/pr.md +7 -6
- package/src/core/commands/research/analyze.md +28 -20
- package/src/core/commands/research/ask.md +43 -0
- package/src/core/commands/research/import.md +29 -21
- package/src/core/commands/research/list.md +8 -7
- package/src/core/commands/research/synthesize.md +356 -20
- package/src/core/commands/research/view.md +8 -5
- package/src/core/commands/review.md +24 -6
- package/src/core/commands/skill/create.md +34 -0
- package/tools/cli/lib/docs-setup.js +4 -0
|
@@ -6,8 +6,51 @@
|
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
+
const crypto = require('crypto');
|
|
9
10
|
const { execFileSync } = require('child_process');
|
|
10
11
|
const { c, log, header, readJSON } = require('./configure-utils');
|
|
12
|
+
const { tryOptional } = require('../../lib/errors');
|
|
13
|
+
const { FEATURES } = require('./configure-features');
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// CONTENT HASH HELPERS
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Hash a file's content using SHA-256 (first 16 hex chars)
|
|
21
|
+
* @param {string} filePath - Path to the file
|
|
22
|
+
* @returns {string|null} 16-char hex hash, or null if file can't be read
|
|
23
|
+
*/
|
|
24
|
+
function hashFile(filePath) {
|
|
25
|
+
try {
|
|
26
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
27
|
+
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Find the directory containing package source scripts.
|
|
35
|
+
* Checks require.resolve first, then common fallback locations.
|
|
36
|
+
* @returns {string|null} Path to the scripts directory, or null
|
|
37
|
+
*/
|
|
38
|
+
function findPackageScriptDir() {
|
|
39
|
+
try {
|
|
40
|
+
const pkgPath = require.resolve('agileflow/package.json');
|
|
41
|
+
return path.join(path.dirname(pkgPath), 'scripts');
|
|
42
|
+
} catch {
|
|
43
|
+
// Fallback: check common locations
|
|
44
|
+
const candidates = [
|
|
45
|
+
path.join(process.cwd(), 'node_modules', 'agileflow', 'scripts'),
|
|
46
|
+
path.join(process.cwd(), 'packages', 'cli', 'scripts'), // monorepo dev
|
|
47
|
+
];
|
|
48
|
+
for (const dir of candidates) {
|
|
49
|
+
if (fs.existsSync(dir)) return dir;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
11
54
|
|
|
12
55
|
// ============================================================================
|
|
13
56
|
// DETECTION
|
|
@@ -64,12 +107,10 @@ function detectConfig(version) {
|
|
|
64
107
|
// Git detection
|
|
65
108
|
if (fs.existsSync('.git')) {
|
|
66
109
|
status.git.initialized = true;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}).trim();
|
|
72
|
-
} catch {}
|
|
110
|
+
status.git.remote = tryOptional(() => execFileSync('git', ['remote', 'get-url', 'origin'], {
|
|
111
|
+
encoding: 'utf8',
|
|
112
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
113
|
+
}).trim(), 'git remote') ?? null;
|
|
73
114
|
}
|
|
74
115
|
|
|
75
116
|
// Settings file detection
|
|
@@ -257,17 +298,51 @@ function detectMetadata(status, version) {
|
|
|
257
298
|
status.features.tmuxautospawn.enabled = true; // Default enabled
|
|
258
299
|
}
|
|
259
300
|
|
|
260
|
-
// Read feature versions and check if outdated
|
|
301
|
+
// Read feature versions and check if outdated (content-based)
|
|
261
302
|
if (meta.features) {
|
|
262
303
|
const featureKeyMap = { askUserQuestion: 'askuserquestion', tmuxAutoSpawn: 'tmuxautospawn' };
|
|
304
|
+
const packageScriptDir = findPackageScriptDir();
|
|
305
|
+
|
|
263
306
|
Object.entries(meta.features).forEach(([feature, data]) => {
|
|
264
307
|
const statusKey = featureKeyMap[feature] || feature.toLowerCase();
|
|
265
308
|
if (status.features[statusKey] && data.version) {
|
|
266
309
|
status.features[statusKey].version = data.version;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
310
|
+
|
|
311
|
+
if (!status.features[statusKey].enabled) return;
|
|
312
|
+
|
|
313
|
+
// Content-based outdated detection
|
|
314
|
+
const featureConfig = FEATURES[statusKey];
|
|
315
|
+
const scriptsToCheck = featureConfig?.scripts
|
|
316
|
+
|| (featureConfig?.script ? [featureConfig.script] : []);
|
|
317
|
+
|
|
318
|
+
if (scriptsToCheck.length > 0 && packageScriptDir) {
|
|
319
|
+
// Compare installed scripts against package source
|
|
320
|
+
let isOutdated = false;
|
|
321
|
+
for (const scriptName of scriptsToCheck) {
|
|
322
|
+
const packageScript = path.join(packageScriptDir, scriptName);
|
|
323
|
+
const installedScript = path.join(
|
|
324
|
+
process.cwd(), '.agileflow', 'scripts', scriptName
|
|
325
|
+
);
|
|
326
|
+
const packageHash = hashFile(packageScript);
|
|
327
|
+
const installedHash = hashFile(installedScript);
|
|
328
|
+
|
|
329
|
+
if (packageHash && installedHash && packageHash !== installedHash) {
|
|
330
|
+
isOutdated = true;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (isOutdated) {
|
|
335
|
+
status.features[statusKey].outdated = true;
|
|
336
|
+
status.hasOutdated = true;
|
|
337
|
+
}
|
|
338
|
+
} else if (featureConfig?.metadataOnly) {
|
|
339
|
+
// Metadata-only features: use version comparison (no scripts to hash)
|
|
340
|
+
if (data.version !== version) {
|
|
341
|
+
status.features[statusKey].outdated = true;
|
|
342
|
+
status.hasOutdated = true;
|
|
343
|
+
}
|
|
270
344
|
}
|
|
345
|
+
// If no package source found or no scripts, don't mark outdated (fail open)
|
|
271
346
|
}
|
|
272
347
|
});
|
|
273
348
|
}
|
|
@@ -403,4 +478,6 @@ module.exports = {
|
|
|
403
478
|
detectPreToolUseHooks,
|
|
404
479
|
detectStatusLine,
|
|
405
480
|
detectMetadata,
|
|
481
|
+
hashFile,
|
|
482
|
+
findPackageScriptDir,
|
|
406
483
|
};
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
+
const crypto = require('crypto');
|
|
9
10
|
const os = require('os');
|
|
10
11
|
const {
|
|
11
12
|
c,
|
|
@@ -53,8 +54,12 @@ const FEATURES = {
|
|
|
53
54
|
description: 'Auto-kill duplicate Claude processes in same directory to prevent freezing',
|
|
54
55
|
},
|
|
55
56
|
claudeflags: {
|
|
56
|
-
metadataOnly:
|
|
57
|
-
description: 'Default flags for Claude CLI (
|
|
57
|
+
metadataOnly: false,
|
|
58
|
+
description: 'Default flags for Claude CLI (sets permissions.defaultMode in .claude/settings.json)',
|
|
59
|
+
},
|
|
60
|
+
agentteams: {
|
|
61
|
+
metadataOnly: false,
|
|
62
|
+
description: 'Enable Claude Code native Agent Teams (sets env var in .claude/settings.json)',
|
|
58
63
|
},
|
|
59
64
|
};
|
|
60
65
|
|
|
@@ -148,6 +153,20 @@ const SCRIPTS_DIR = path.join(process.cwd(), '.agileflow', 'scripts');
|
|
|
148
153
|
const scriptExists = scriptName => fs.existsSync(path.join(SCRIPTS_DIR, scriptName));
|
|
149
154
|
const getScriptPath = scriptName => `.agileflow/scripts/${scriptName}`;
|
|
150
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Hash a file's content using SHA-256 (first 16 hex chars)
|
|
158
|
+
* @param {string} filePath - Path to the file
|
|
159
|
+
* @returns {string|null} 16-char hex hash, or null if file can't be read
|
|
160
|
+
*/
|
|
161
|
+
function hashFile(filePath) {
|
|
162
|
+
try {
|
|
163
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
164
|
+
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
151
170
|
// ============================================================================
|
|
152
171
|
// METADATA MANAGEMENT
|
|
153
172
|
// ============================================================================
|
|
@@ -304,8 +323,24 @@ function enableFeature(feature, options = {}, version) {
|
|
|
304
323
|
}
|
|
305
324
|
|
|
306
325
|
// Handle claude flags (e.g., --dangerously-skip-permissions)
|
|
326
|
+
// Also sets permissions.defaultMode in .claude/settings.json
|
|
307
327
|
if (feature === 'claudeflags') {
|
|
308
328
|
const defaultFlags = options.flags || '--dangerously-skip-permissions';
|
|
329
|
+
|
|
330
|
+
// Map CLI flags to settings.json defaultMode values
|
|
331
|
+
const flagToMode = {
|
|
332
|
+
'--dangerously-skip-permissions': 'bypassPermissions',
|
|
333
|
+
'--permission-mode acceptEdits': 'acceptEdits',
|
|
334
|
+
};
|
|
335
|
+
const defaultMode = flagToMode[defaultFlags];
|
|
336
|
+
|
|
337
|
+
if (defaultMode) {
|
|
338
|
+
settings.permissions = settings.permissions || {};
|
|
339
|
+
settings.permissions.defaultMode = defaultMode;
|
|
340
|
+
writeJSON('.claude/settings.json', settings);
|
|
341
|
+
info(`Set permissions.defaultMode = "${defaultMode}" in .claude/settings.json`);
|
|
342
|
+
}
|
|
343
|
+
|
|
309
344
|
updateMetadata(
|
|
310
345
|
{
|
|
311
346
|
features: {
|
|
@@ -321,6 +356,33 @@ function enableFeature(feature, options = {}, version) {
|
|
|
321
356
|
);
|
|
322
357
|
success(`Default Claude flags configured: ${defaultFlags}`);
|
|
323
358
|
info('These flags will be passed to Claude when launched via "af" or "agileflow"');
|
|
359
|
+
if (defaultMode) {
|
|
360
|
+
info('Restart Claude Code for the new default mode to take effect');
|
|
361
|
+
}
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Handle agent teams - set env var in .claude/settings.json
|
|
366
|
+
if (feature === 'agentteams') {
|
|
367
|
+
settings.env = settings.env || {};
|
|
368
|
+
settings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = '1';
|
|
369
|
+
writeJSON('.claude/settings.json', settings);
|
|
370
|
+
updateMetadata(
|
|
371
|
+
{
|
|
372
|
+
features: {
|
|
373
|
+
agentTeams: {
|
|
374
|
+
enabled: true,
|
|
375
|
+
version,
|
|
376
|
+
at: new Date().toISOString(),
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
version
|
|
381
|
+
);
|
|
382
|
+
success('Native Agent Teams enabled');
|
|
383
|
+
info('Set CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 in .claude/settings.json');
|
|
384
|
+
info('Claude Code will use native TeamCreate/SendMessage tools');
|
|
385
|
+
info('Fallback: subagent mode (Task/TaskOutput) when native is unavailable');
|
|
324
386
|
return true;
|
|
325
387
|
}
|
|
326
388
|
|
|
@@ -391,9 +453,18 @@ function enableFeature(feature, options = {}, version) {
|
|
|
391
453
|
return enableDamageControl(settings, options, version);
|
|
392
454
|
}
|
|
393
455
|
|
|
456
|
+
const featureConfig = FEATURES[feature];
|
|
457
|
+
const contentHash = featureConfig?.script
|
|
458
|
+
? hashFile(path.join(SCRIPTS_DIR, featureConfig.script))
|
|
459
|
+
: null;
|
|
394
460
|
writeJSON('.claude/settings.json', settings);
|
|
395
461
|
updateMetadata(
|
|
396
|
-
{ features: { [feature]: {
|
|
462
|
+
{ features: { [feature]: {
|
|
463
|
+
enabled: true,
|
|
464
|
+
version,
|
|
465
|
+
...(contentHash ? { contentHash } : {}),
|
|
466
|
+
at: new Date().toISOString(),
|
|
467
|
+
} } },
|
|
397
468
|
version
|
|
398
469
|
);
|
|
399
470
|
updateGitignore();
|
|
@@ -558,6 +629,7 @@ function enableDamageControl(settings, options, version) {
|
|
|
558
629
|
|
|
559
630
|
success('Damage control PreToolUse hooks enabled');
|
|
560
631
|
|
|
632
|
+
const primaryHash = hashFile(path.join(SCRIPTS_DIR, 'damage-control-bash.js'));
|
|
561
633
|
updateMetadata(
|
|
562
634
|
{
|
|
563
635
|
features: {
|
|
@@ -565,6 +637,7 @@ function enableDamageControl(settings, options, version) {
|
|
|
565
637
|
enabled: true,
|
|
566
638
|
protectionLevel: level,
|
|
567
639
|
version,
|
|
640
|
+
...(primaryHash ? { contentHash: primaryHash } : {}),
|
|
568
641
|
at: new Date().toISOString(),
|
|
569
642
|
},
|
|
570
643
|
},
|
|
@@ -710,8 +783,13 @@ function disableFeature(feature, version) {
|
|
|
710
783
|
return true;
|
|
711
784
|
}
|
|
712
785
|
|
|
713
|
-
// Disable claude flags
|
|
786
|
+
// Disable claude flags - also reset permissions.defaultMode in settings.json
|
|
714
787
|
if (feature === 'claudeflags') {
|
|
788
|
+
if (settings.permissions?.defaultMode) {
|
|
789
|
+
delete settings.permissions.defaultMode;
|
|
790
|
+
writeJSON('.claude/settings.json', settings);
|
|
791
|
+
info('Removed permissions.defaultMode from .claude/settings.json');
|
|
792
|
+
}
|
|
715
793
|
updateMetadata(
|
|
716
794
|
{
|
|
717
795
|
features: {
|
|
@@ -727,6 +805,34 @@ function disableFeature(feature, version) {
|
|
|
727
805
|
);
|
|
728
806
|
success('Default Claude flags disabled');
|
|
729
807
|
info('Claude will launch with default permissions (prompts for each action)');
|
|
808
|
+
info('Restart Claude Code for the change to take effect');
|
|
809
|
+
return true;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Disable agent teams - remove env var from .claude/settings.json
|
|
813
|
+
if (feature === 'agentteams') {
|
|
814
|
+
if (settings.env) {
|
|
815
|
+
delete settings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS;
|
|
816
|
+
if (Object.keys(settings.env).length === 0) {
|
|
817
|
+
delete settings.env;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
writeJSON('.claude/settings.json', settings);
|
|
821
|
+
updateMetadata(
|
|
822
|
+
{
|
|
823
|
+
features: {
|
|
824
|
+
agentTeams: {
|
|
825
|
+
enabled: false,
|
|
826
|
+
version,
|
|
827
|
+
at: new Date().toISOString(),
|
|
828
|
+
},
|
|
829
|
+
},
|
|
830
|
+
},
|
|
831
|
+
version
|
|
832
|
+
);
|
|
833
|
+
success('Native Agent Teams disabled');
|
|
834
|
+
info('Removed CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS from .claude/settings.json');
|
|
835
|
+
info('AgileFlow will use subagent mode (Task/TaskOutput) for multi-agent orchestration');
|
|
730
836
|
return true;
|
|
731
837
|
}
|
|
732
838
|
|
|
@@ -9,6 +9,7 @@ const path = require('path');
|
|
|
9
9
|
const crypto = require('crypto');
|
|
10
10
|
const { execFileSync } = require('child_process');
|
|
11
11
|
const { feedback } = require('../../lib/feedback');
|
|
12
|
+
const { tryOptional } = require('../../lib/errors');
|
|
12
13
|
const {
|
|
13
14
|
c,
|
|
14
15
|
log,
|
|
@@ -115,11 +116,11 @@ function listScripts() {
|
|
|
115
116
|
// Check if modified
|
|
116
117
|
let isModified = false;
|
|
117
118
|
if (exists && fileIndex?.files?.[`scripts/${script}`]) {
|
|
118
|
-
|
|
119
|
+
isModified = tryOptional(() => {
|
|
119
120
|
const currentHash = sha256(fs.readFileSync(scriptPath));
|
|
120
121
|
const indexHash = fileIndex.files[`scripts/${script}`].sha256;
|
|
121
|
-
|
|
122
|
-
}
|
|
122
|
+
return currentHash !== indexHash;
|
|
123
|
+
}, 'hash check') ?? false;
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
// Print status
|
|
@@ -279,9 +280,7 @@ function repairScripts(targetFeature = null) {
|
|
|
279
280
|
if (fs.existsSync(srcPath)) {
|
|
280
281
|
try {
|
|
281
282
|
fs.copyFileSync(srcPath, destPath);
|
|
282
|
-
|
|
283
|
-
fs.chmodSync(destPath, 0o755);
|
|
284
|
-
} catch {}
|
|
283
|
+
tryOptional(() => fs.chmodSync(destPath, 0o755), 'chmod');
|
|
285
284
|
success(`Restored ${script}`);
|
|
286
285
|
repaired++;
|
|
287
286
|
} catch (err) {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
+
const { tryOptional } = require('../../lib/errors');
|
|
9
10
|
|
|
10
11
|
// ============================================================================
|
|
11
12
|
// COLORS & LOGGING
|
|
@@ -58,9 +59,7 @@ const copyTemplate = (templateName, destPath) => {
|
|
|
58
59
|
for (const src of sources) {
|
|
59
60
|
if (fs.existsSync(src)) {
|
|
60
61
|
fs.copyFileSync(src, destPath);
|
|
61
|
-
|
|
62
|
-
fs.chmodSync(destPath, '755');
|
|
63
|
-
} catch {}
|
|
62
|
+
tryOptional(() => fs.chmodSync(destPath, '755'), 'chmod');
|
|
64
63
|
return true;
|
|
65
64
|
}
|
|
66
65
|
}
|
|
@@ -321,6 +321,23 @@ function generateSummary(prefetched = null, options = {}) {
|
|
|
321
321
|
|
|
322
322
|
summary += divider();
|
|
323
323
|
|
|
324
|
+
// Scale indicator (EP-0033)
|
|
325
|
+
let scaleDetectorSummary;
|
|
326
|
+
try { scaleDetectorSummary = require('./scale-detector'); } catch { /* not available */ }
|
|
327
|
+
if (scaleDetectorSummary) {
|
|
328
|
+
try {
|
|
329
|
+
const scaleResult = scaleDetectorSummary.detectScale({
|
|
330
|
+
rootDir: process.cwd(),
|
|
331
|
+
statusJson: prefetched?.json?.statusJson,
|
|
332
|
+
sessionState: prefetched?.json?.sessionState,
|
|
333
|
+
});
|
|
334
|
+
const label = scaleDetectorSummary.getScaleLabel(scaleResult.scale);
|
|
335
|
+
summary += row('Scale', label, C.lavender, C.cyan);
|
|
336
|
+
} catch {
|
|
337
|
+
// Silently ignore
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
324
341
|
// Key files
|
|
325
342
|
const keyFileChecks = [
|
|
326
343
|
{ path: 'CLAUDE.md', label: 'CLAUDE' },
|
|
@@ -378,7 +395,7 @@ function generateSummary(prefetched = null, options = {}) {
|
|
|
378
395
|
* @returns {string} Full content
|
|
379
396
|
*/
|
|
380
397
|
function generateFullContent(prefetched = null, options = {}) {
|
|
381
|
-
const { commandName = null, activeSections = [] } = options;
|
|
398
|
+
const { commandName = null, activeSections = [], smartDetectResults = null } = options;
|
|
382
399
|
|
|
383
400
|
let content = '';
|
|
384
401
|
|
|
@@ -437,12 +454,17 @@ function generateFullContent(prefetched = null, options = {}) {
|
|
|
437
454
|
|
|
438
455
|
if (askUserQuestionConfig?.enabled) {
|
|
439
456
|
content += `${C.coral}${C.bold}┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓${C.reset}\n`;
|
|
440
|
-
content += `${C.coral}${C.bold}┃ 🔔
|
|
457
|
+
content += `${C.coral}${C.bold}┃ 🔔 SMART AskUserQuestion After EVERY Response ┃${C.reset}\n`;
|
|
441
458
|
content += `${C.coral}${C.bold}┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛${C.reset}\n`;
|
|
442
459
|
content += `${C.bold}After completing ANY task${C.reset} (implementation, fix, etc.):\n`;
|
|
443
|
-
content += `${C.mintGreen}→ ALWAYS${C.reset} call ${C.skyBlue}AskUserQuestion${C.reset} tool
|
|
444
|
-
content += `${C.coral}→ NEVER${C.reset}
|
|
445
|
-
content += `${C.
|
|
460
|
+
content += `${C.mintGreen}→ ALWAYS${C.reset} call ${C.skyBlue}AskUserQuestion${C.reset} tool with ${C.bold}SMART, contextual${C.reset} options\n`;
|
|
461
|
+
content += `${C.coral}→ NEVER${C.reset} generic options like "Continue" or "What next?"\n\n`;
|
|
462
|
+
content += `${C.bold}Smart Suggestion Principles:${C.reset}\n`;
|
|
463
|
+
content += `${C.mintGreen}→${C.reset} Always mark one option ${C.bold}"(Recommended)"${C.reset} based on logical next step\n`;
|
|
464
|
+
content += `${C.mintGreen}→${C.reset} Be specific: ${C.dim}"Run npm test for auth changes"${C.reset} not ${C.dim}"Run tests"${C.reset}\n`;
|
|
465
|
+
content += `${C.mintGreen}→${C.reset} Include context: ${C.dim}"3 files changed"${C.reset}, story IDs, test results\n`;
|
|
466
|
+
content += `${C.mintGreen}→${C.reset} Suggest workflow progression: plan → implement → test → commit\n`;
|
|
467
|
+
content += `${C.mintGreen}→${C.reset} After errors: suggest specific alternative, not ${C.dim}"fix it"${C.reset}\n\n`;
|
|
446
468
|
}
|
|
447
469
|
|
|
448
470
|
// CONTEXT BUDGET WARNING
|
|
@@ -479,6 +501,27 @@ function generateFullContent(prefetched = null, options = {}) {
|
|
|
479
501
|
content += '\n';
|
|
480
502
|
}
|
|
481
503
|
|
|
504
|
+
// SCALE DETECTION (EP-0033)
|
|
505
|
+
let scaleDetector;
|
|
506
|
+
try { scaleDetector = require('./scale-detector'); } catch { /* not available */ }
|
|
507
|
+
if (scaleDetector) {
|
|
508
|
+
try {
|
|
509
|
+
const scaleResult = scaleDetector.detectScale({
|
|
510
|
+
rootDir: process.cwd(),
|
|
511
|
+
statusJson: prefetched?.json?.statusJson,
|
|
512
|
+
sessionState: prefetched?.json?.sessionState,
|
|
513
|
+
});
|
|
514
|
+
const recs = scaleDetector.getScaleRecommendations(scaleResult.scale);
|
|
515
|
+
const label = scaleDetector.getScaleLabel(scaleResult.scale);
|
|
516
|
+
content += `\n${C.cyan}${C.bold}═══ Project Scale: ${label} ═══${C.reset}\n`;
|
|
517
|
+
content += `${C.dim}Detected: ${scaleResult.metrics.files} files, ${scaleResult.metrics.stories} stories, ${scaleResult.metrics.commits} commits (6mo)${C.reset}\n`;
|
|
518
|
+
content += `Planning depth: ${C.mintGreen}${recs.planningDepth}${C.reset} | Experts: ${C.mintGreen}${recs.expertCount}${C.reset}\n`;
|
|
519
|
+
content += `${C.dim}${recs.description}${C.reset}\n`;
|
|
520
|
+
} catch {
|
|
521
|
+
// Silently ignore scale detection errors
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
482
525
|
// GIT STATUS
|
|
483
526
|
content += `\n${C.skyBlue}${C.bold}═══ Git Status ═══${C.reset}\n`;
|
|
484
527
|
const branch = prefetched?.git?.branch ?? safeExec('git branch --show-current') ?? 'unknown';
|
|
@@ -533,7 +576,7 @@ function generateFullContent(prefetched = null, options = {}) {
|
|
|
533
576
|
if (Array.isArray(sessionState.active_commands) && sessionState.active_commands.length > 0) {
|
|
534
577
|
const cmdNames = sessionState.active_commands.map(c => c.name).join(', ');
|
|
535
578
|
content += `Active commands: ${C.skyBlue}${cmdNames}${C.reset}\n`;
|
|
536
|
-
} else if (sessionState.active_command) {
|
|
579
|
+
} else if (sessionState.active_command && sessionState.active_command.name) {
|
|
537
580
|
content += `Active command: ${C.skyBlue}${sessionState.active_command.name}${C.reset}\n`;
|
|
538
581
|
}
|
|
539
582
|
|
|
@@ -592,7 +635,7 @@ function generateRemainingContent(prefetched, options = {}) {
|
|
|
592
635
|
const sessionDir = story.claimedBy?.path
|
|
593
636
|
? path.basename(story.claimedBy.path)
|
|
594
637
|
: 'unknown';
|
|
595
|
-
content += ` ${C.coral}🔒${C.reset} ${C.lavender}${story.id}${C.reset} "${story.title}" ${C.dim}→ Session ${story.claimedBy?.session_id
|
|
638
|
+
content += ` ${C.coral}🔒${C.reset} ${C.lavender}${story.id ?? '?'}${C.reset} "${story.title ?? 'untitled'}" ${C.dim}→ Session ${story.claimedBy?.session_id ?? '?'} (${sessionDir})${C.reset}\n`;
|
|
596
639
|
});
|
|
597
640
|
content += '\n';
|
|
598
641
|
}
|
|
@@ -601,7 +644,7 @@ function generateRemainingContent(prefetched, options = {}) {
|
|
|
601
644
|
if (myResult.ok && myResult.stories && myResult.stories.length > 0) {
|
|
602
645
|
content += `\n${C.mintGreen}${C.bold}═══ ✓ Your Claimed Stories ═══${C.reset}\n`;
|
|
603
646
|
myResult.stories.forEach(story => {
|
|
604
|
-
content += ` ${C.mintGreen}✓${C.reset} ${C.lavender}${story.id}${C.reset} "${story.title}"\n`;
|
|
647
|
+
content += ` ${C.mintGreen}✓${C.reset} ${C.lavender}${story.id ?? '?'}${C.reset} "${story.title ?? 'untitled'}"\n`;
|
|
605
648
|
});
|
|
606
649
|
content += '\n';
|
|
607
650
|
}
|
|
@@ -813,6 +856,42 @@ function generateRemainingContent(prefetched, options = {}) {
|
|
|
813
856
|
}
|
|
814
857
|
}
|
|
815
858
|
|
|
859
|
+
// SMART RECOMMENDATIONS (Contextual Feature Router)
|
|
860
|
+
const smartDetectResults = options.smartDetectResults;
|
|
861
|
+
if (smartDetectResults && !smartDetectResults.disabled) {
|
|
862
|
+
const { immediate, available, auto_enabled } = smartDetectResults.recommendations;
|
|
863
|
+
const hasRecommendations = immediate.length > 0 || available.length > 0;
|
|
864
|
+
|
|
865
|
+
if (hasRecommendations) {
|
|
866
|
+
content += `\n${C.brand}${C.bold}═══ Smart Recommendations ═══${C.reset}\n`;
|
|
867
|
+
content += `${C.dim}Phase: ${smartDetectResults.lifecycle_phase} (${smartDetectResults.phase_reason})${C.reset}\n`;
|
|
868
|
+
|
|
869
|
+
if (immediate.length > 0) {
|
|
870
|
+
content += `\n${C.coral}${C.bold}Immediate:${C.reset}\n`;
|
|
871
|
+
immediate.forEach(r => {
|
|
872
|
+
content += ` ${C.coral}!${C.reset} ${C.bold}${r.feature}${C.reset}: ${r.trigger} ${C.dim}(${r.command})${C.reset}\n`;
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (available.length > 0) {
|
|
877
|
+
content += `\n${C.skyBlue}Available:${C.reset}\n`;
|
|
878
|
+
available.slice(0, 5).forEach(r => {
|
|
879
|
+
content += ` ${C.skyBlue}>${C.reset} ${r.feature}: ${r.trigger} ${C.dim}(${r.command})${C.reset}\n`;
|
|
880
|
+
});
|
|
881
|
+
if (available.length > 5) {
|
|
882
|
+
content += ` ${C.dim}... and ${available.length - 5} more${C.reset}\n`;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Show auto-enabled modes
|
|
888
|
+
const modes = auto_enabled || {};
|
|
889
|
+
const enabledModes = Object.entries(modes).filter(([, v]) => v).map(([k]) => k.replace('_', ' '));
|
|
890
|
+
if (enabledModes.length > 0) {
|
|
891
|
+
content += `\n${C.mintGreen}Auto-enabled:${C.reset} ${enabledModes.join(', ')}\n`;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
816
895
|
// FOOTER
|
|
817
896
|
content += `\n${C.dim}─────────────────────────────────────────${C.reset}\n`;
|
|
818
897
|
content += `${C.dim}Context gathered in single execution. Claude has full context.${C.reset}\n`;
|
|
@@ -33,6 +33,14 @@ const c = {
|
|
|
33
33
|
reset: '\x1b[0m',
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
// Pattern cache: avoids re-reading and re-parsing YAML on every hook invocation.
|
|
37
|
+
// Invalidated when the config file's mtime changes.
|
|
38
|
+
const _patternCache = {
|
|
39
|
+
/** @type {string|null} */ filePath: null,
|
|
40
|
+
/** @type {number} */ mtime: 0,
|
|
41
|
+
/** @type {object|null} */ config: null,
|
|
42
|
+
};
|
|
43
|
+
|
|
36
44
|
// Shared constants
|
|
37
45
|
const CONFIG_PATHS = [
|
|
38
46
|
'.agileflow/config/damage-control-patterns.yaml',
|
|
@@ -70,8 +78,9 @@ function expandPath(p) {
|
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
/**
|
|
73
|
-
* Load patterns configuration from YAML file
|
|
74
|
-
* Returns
|
|
81
|
+
* Load patterns configuration from YAML file with caching.
|
|
82
|
+
* Returns cached config when the file hasn't changed (mtime check).
|
|
83
|
+
* Returns empty config if not found (fail-open).
|
|
75
84
|
*
|
|
76
85
|
* @param {string} projectRoot - Project root directory
|
|
77
86
|
* @param {function} parseYAML - Function to parse YAML content
|
|
@@ -83,8 +92,23 @@ function loadPatterns(projectRoot, parseYAML, defaultConfig = {}) {
|
|
|
83
92
|
const fullPath = path.join(projectRoot, configPath);
|
|
84
93
|
if (fs.existsSync(fullPath)) {
|
|
85
94
|
try {
|
|
95
|
+
// Check mtime for cache invalidation
|
|
96
|
+
const stat = fs.statSync(fullPath);
|
|
97
|
+
const mtime = stat.mtimeMs;
|
|
98
|
+
|
|
99
|
+
if (_patternCache.filePath === fullPath && _patternCache.mtime === mtime && _patternCache.config) {
|
|
100
|
+
return _patternCache.config;
|
|
101
|
+
}
|
|
102
|
+
|
|
86
103
|
const content = fs.readFileSync(fullPath, 'utf8');
|
|
87
|
-
|
|
104
|
+
const config = parseYAML(content);
|
|
105
|
+
|
|
106
|
+
// Store in cache
|
|
107
|
+
_patternCache.filePath = fullPath;
|
|
108
|
+
_patternCache.mtime = mtime;
|
|
109
|
+
_patternCache.config = config;
|
|
110
|
+
|
|
111
|
+
return config;
|
|
88
112
|
} catch (e) {
|
|
89
113
|
// Continue to next path
|
|
90
114
|
}
|
|
@@ -95,6 +119,15 @@ function loadPatterns(projectRoot, parseYAML, defaultConfig = {}) {
|
|
|
95
119
|
return defaultConfig;
|
|
96
120
|
}
|
|
97
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Clear the pattern cache (for testing or forced reload).
|
|
124
|
+
*/
|
|
125
|
+
function clearPatternCache() {
|
|
126
|
+
_patternCache.filePath = null;
|
|
127
|
+
_patternCache.mtime = 0;
|
|
128
|
+
_patternCache.config = null;
|
|
129
|
+
}
|
|
130
|
+
|
|
98
131
|
/**
|
|
99
132
|
* Check if a file path matches any of the protected patterns
|
|
100
133
|
*
|
|
@@ -559,6 +592,7 @@ module.exports = {
|
|
|
559
592
|
findProjectRoot,
|
|
560
593
|
expandPath,
|
|
561
594
|
loadPatterns,
|
|
595
|
+
clearPatternCache,
|
|
562
596
|
pathMatches,
|
|
563
597
|
outputBlocked,
|
|
564
598
|
runDamageControlHook,
|