mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-30 16:45:48 +08:00
fix: inject learned skills at session start
This commit is contained in:
parent
0c61710c43
commit
d26d66fd3b
@ -30,6 +30,8 @@ const fs = require('fs');
|
||||
|
||||
const INSTINCT_CONFIDENCE_THRESHOLD = 0.7;
|
||||
const MAX_INJECTED_INSTINCTS = 6;
|
||||
const MAX_INJECTED_LEARNED_SKILLS = 6;
|
||||
const MAX_LEARNED_SKILL_SUMMARY_CHARS = 220;
|
||||
const DEFAULT_SESSION_RETENTION_DAYS = 30;
|
||||
|
||||
/**
|
||||
@ -347,6 +349,119 @@ function summarizeActiveInstincts(observerContext) {
|
||||
return `Active instincts:\n${lines.join('\n')}`;
|
||||
}
|
||||
|
||||
function stripMarkdownInline(value) {
|
||||
return String(value || '')
|
||||
.replace(/`([^`]+)`/g, '$1')
|
||||
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
||||
.replace(/\*([^*]+)\*/g, '$1')
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function collapseWhitespace(value) {
|
||||
return String(value || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function truncateSummary(value, maxLength = MAX_LEARNED_SKILL_SUMMARY_CHARS) {
|
||||
const normalized = collapseWhitespace(stripMarkdownInline(value));
|
||||
if (normalized.length <= maxLength) {
|
||||
return normalized;
|
||||
}
|
||||
return `${normalized.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
|
||||
}
|
||||
|
||||
function extractMarkdownHeading(content) {
|
||||
const match = String(content || '').match(/^#\s+(.+)$/m);
|
||||
return match ? stripMarkdownInline(match[1]) : '';
|
||||
}
|
||||
|
||||
function extractSection(content, headingPattern) {
|
||||
const source = String(content || '');
|
||||
const match = source.match(new RegExp(`^##\\s+${headingPattern}\\s*\\n+([\\s\\S]+?)(?:\\n##\\s+|$)`, 'im'));
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
function extractFirstParagraph(content) {
|
||||
const withoutHeading = String(content || '').replace(/^#\s+.+$/m, '').trim();
|
||||
return withoutHeading
|
||||
.split(/\n\s*\n/)
|
||||
.map(paragraph => paragraph.trim())
|
||||
.find(Boolean) || '';
|
||||
}
|
||||
|
||||
function summarizeLearnedSkillFile(filePath, learnedRoot) {
|
||||
const content = readFile(filePath);
|
||||
if (!content) return null;
|
||||
|
||||
const isDirectorySkill = path.basename(filePath).toLowerCase() === 'skill.md';
|
||||
const slug = isDirectorySkill
|
||||
? path.basename(path.dirname(filePath))
|
||||
: path.basename(filePath, path.extname(filePath));
|
||||
const title = extractMarkdownHeading(content) || slug;
|
||||
const summary = truncateSummary(
|
||||
extractSection(content, 'When to Use')
|
||||
|| extractSection(content, 'Trigger')
|
||||
|| extractSection(content, 'Problem')
|
||||
|| extractFirstParagraph(content)
|
||||
|| title
|
||||
);
|
||||
|
||||
if (!summary) return null;
|
||||
|
||||
let mtime = 0;
|
||||
try {
|
||||
mtime = fs.statSync(filePath).mtimeMs;
|
||||
} catch {
|
||||
// Keep unreadable/deleted files out of recency priority without failing the hook.
|
||||
}
|
||||
|
||||
const relativePath = path.relative(learnedRoot, filePath);
|
||||
return {
|
||||
slug,
|
||||
title: truncateSummary(title, 80),
|
||||
summary,
|
||||
relativePath,
|
||||
mtime,
|
||||
};
|
||||
}
|
||||
|
||||
function collectLearnedSkillFiles(learnedDir) {
|
||||
const flatMarkdownFiles = findFiles(learnedDir, '*.md');
|
||||
const directorySkillFiles = findFiles(learnedDir, 'SKILL.md', { recursive: true });
|
||||
const byPath = new Map();
|
||||
|
||||
for (const match of [...flatMarkdownFiles, ...directorySkillFiles]) {
|
||||
byPath.set(match.path, match);
|
||||
}
|
||||
|
||||
return Array.from(byPath.values())
|
||||
.sort((left, right) => right.mtime - left.mtime || left.path.localeCompare(right.path));
|
||||
}
|
||||
|
||||
function summarizeLearnedSkills(learnedDir, learnedSkillFiles = collectLearnedSkillFiles(learnedDir)) {
|
||||
const summaries = learnedSkillFiles
|
||||
.map(match => summarizeLearnedSkillFile(match.path, learnedDir))
|
||||
.filter(Boolean)
|
||||
.slice(0, MAX_INJECTED_LEARNED_SKILLS);
|
||||
|
||||
if (summaries.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
log(`[SessionStart] Injecting ${summaries.length} learned skill(s) into session context`);
|
||||
|
||||
const lines = summaries.map(skill => {
|
||||
const titleSuffix = skill.title && skill.title !== skill.slug ? ` (${skill.title})` : '';
|
||||
return `- ${skill.slug}${titleSuffix}: ${skill.summary}`;
|
||||
});
|
||||
|
||||
return [
|
||||
'Available learned skills:',
|
||||
'Reference only; apply a learned skill only when it is relevant to the current user request.',
|
||||
...lines,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const sessionsDir = getSessionsDir();
|
||||
const sessionSearchDirs = getSessionSearchDirs();
|
||||
@ -428,12 +543,17 @@ async function main() {
|
||||
}
|
||||
|
||||
// Check for learned skills
|
||||
const learnedSkills = findFiles(learnedDir, '*.md');
|
||||
const learnedSkills = collectLearnedSkillFiles(learnedDir);
|
||||
|
||||
if (learnedSkills.length > 0) {
|
||||
log(`[SessionStart] ${learnedSkills.length} learned skill(s) available in ${learnedDir}`);
|
||||
}
|
||||
|
||||
const learnedSkillSummary = summarizeLearnedSkills(learnedDir, learnedSkills);
|
||||
if (learnedSkillSummary) {
|
||||
additionalContextParts.push(learnedSkillSummary);
|
||||
}
|
||||
|
||||
// Check for available session aliases
|
||||
const aliases = listAliases({ limit: 5 });
|
||||
|
||||
|
||||
@ -545,6 +545,65 @@ async function runTests() {
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
await asyncTest('injects learned skills into session-start additional context', async () => {
|
||||
const isoHome = path.join(os.tmpdir(), `ecc-skills-context-${Date.now()}`);
|
||||
const learnedDir = path.join(isoHome, '.claude', 'skills', 'learned');
|
||||
fs.mkdirSync(learnedDir, { recursive: true });
|
||||
fs.mkdirSync(getCanonicalSessionsDir(isoHome), { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(learnedDir, 'testing-patterns.md'),
|
||||
[
|
||||
'# Testing Patterns',
|
||||
'',
|
||||
'## When to Use',
|
||||
'Use for recurring flaky integration tests that need deterministic setup checks.',
|
||||
'',
|
||||
'## Solution',
|
||||
'Verify service readiness before running the test body.',
|
||||
].join('\n'),
|
||||
);
|
||||
fs.mkdirSync(path.join(learnedDir, 'debugging-pattern'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(learnedDir, 'debugging-pattern', 'SKILL.md'),
|
||||
[
|
||||
'# Debugging Pattern',
|
||||
'',
|
||||
'## Trigger',
|
||||
'Use when a CLI tool silently exits without a result payload.',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await runScript(path.join(scriptsDir, 'session-start.js'), '', {
|
||||
HOME: isoHome,
|
||||
USERPROFILE: isoHome
|
||||
});
|
||||
assert.strictEqual(result.code, 0);
|
||||
const additionalContext = getSessionStartAdditionalContext(result.stdout);
|
||||
assert.ok(
|
||||
additionalContext.includes('Available learned skills'),
|
||||
`Should inject learned skills into additionalContext, got: ${additionalContext}`
|
||||
);
|
||||
assert.ok(additionalContext.includes('testing-patterns'), 'Should include the learned skill slug');
|
||||
assert.ok(
|
||||
additionalContext.includes('Use for recurring flaky integration tests'),
|
||||
'Should include the learned skill trigger text'
|
||||
);
|
||||
assert.ok(additionalContext.includes('debugging-pattern'), 'Should include directory-style learned skills');
|
||||
assert.ok(
|
||||
additionalContext.includes('CLI tool silently exits'),
|
||||
'Should summarize directory-style learned skill trigger text'
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(isoHome, { recursive: true, force: true });
|
||||
}
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
// check-console-log.js tests
|
||||
console.log('\ncheck-console-log.js:');
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user