agileflow 2.99.7 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/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 +184 -133
- 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/auto-self-improve.js +3 -3
- package/scripts/claude-smart.sh +67 -0
- package/scripts/claude-tmux.sh +248 -170
- package/scripts/damage-control-multi-agent.js +227 -0
- package/scripts/lib/bus-utils.js +471 -0
- package/scripts/lib/configure-detect.js +5 -6
- package/scripts/lib/configure-features.js +44 -0
- 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/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 +105 -2
- 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
|
@@ -8,6 +8,7 @@ const fs = require('fs');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const { execFileSync } = require('child_process');
|
|
10
10
|
const { c, log, header, readJSON } = require('./configure-utils');
|
|
11
|
+
const { tryOptional } = require('../../lib/errors');
|
|
11
12
|
|
|
12
13
|
// ============================================================================
|
|
13
14
|
// DETECTION
|
|
@@ -64,12 +65,10 @@ function detectConfig(version) {
|
|
|
64
65
|
// Git detection
|
|
65
66
|
if (fs.existsSync('.git')) {
|
|
66
67
|
status.git.initialized = true;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}).trim();
|
|
72
|
-
} catch {}
|
|
68
|
+
status.git.remote = tryOptional(() => execFileSync('git', ['remote', 'get-url', 'origin'], {
|
|
69
|
+
encoding: 'utf8',
|
|
70
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
71
|
+
}).trim(), 'git remote') ?? null;
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
// Settings file detection
|
|
@@ -56,6 +56,10 @@ const FEATURES = {
|
|
|
56
56
|
metadataOnly: true,
|
|
57
57
|
description: 'Default flags for Claude CLI (e.g., --dangerously-skip-permissions)',
|
|
58
58
|
},
|
|
59
|
+
agentteams: {
|
|
60
|
+
metadataOnly: true,
|
|
61
|
+
description: 'Enable Claude Code native Agent Teams (experimental multi-agent orchestration)',
|
|
62
|
+
},
|
|
59
63
|
};
|
|
60
64
|
|
|
61
65
|
const PROFILES = {
|
|
@@ -324,6 +328,27 @@ function enableFeature(feature, options = {}, version) {
|
|
|
324
328
|
return true;
|
|
325
329
|
}
|
|
326
330
|
|
|
331
|
+
// Handle agent teams (metadata only)
|
|
332
|
+
if (feature === 'agentteams') {
|
|
333
|
+
updateMetadata(
|
|
334
|
+
{
|
|
335
|
+
features: {
|
|
336
|
+
agentTeams: {
|
|
337
|
+
enabled: true,
|
|
338
|
+
version,
|
|
339
|
+
at: new Date().toISOString(),
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
version
|
|
344
|
+
);
|
|
345
|
+
success('Native Agent Teams enabled');
|
|
346
|
+
info('Claude Code will use native TeamCreate/SendMessage tools when available');
|
|
347
|
+
info('Requires: CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 (set by Claude Code)');
|
|
348
|
+
info('Fallback: subagent mode (Task/TaskOutput) when native is unavailable');
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
|
|
327
352
|
// Handle shell aliases
|
|
328
353
|
if (feature === 'shellaliases') {
|
|
329
354
|
const result = enableShellAliases();
|
|
@@ -730,6 +755,25 @@ function disableFeature(feature, version) {
|
|
|
730
755
|
return true;
|
|
731
756
|
}
|
|
732
757
|
|
|
758
|
+
// Disable agent teams
|
|
759
|
+
if (feature === 'agentteams') {
|
|
760
|
+
updateMetadata(
|
|
761
|
+
{
|
|
762
|
+
features: {
|
|
763
|
+
agentTeams: {
|
|
764
|
+
enabled: false,
|
|
765
|
+
version,
|
|
766
|
+
at: new Date().toISOString(),
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
},
|
|
770
|
+
version
|
|
771
|
+
);
|
|
772
|
+
success('Native Agent Teams disabled');
|
|
773
|
+
info('AgileFlow will use subagent mode (Task/TaskOutput) for multi-agent orchestration');
|
|
774
|
+
return true;
|
|
775
|
+
}
|
|
776
|
+
|
|
733
777
|
// Disable shell aliases
|
|
734
778
|
if (feature === 'shellaliases') {
|
|
735
779
|
const result = disableShellAliases();
|
|
@@ -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,
|