mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-12 20:53:34 +08:00
feat: inject active instincts into session start context
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
getClaudeDir,
|
||||||
getSessionsDir,
|
getSessionsDir,
|
||||||
getSessionSearchDirs,
|
getSessionSearchDirs,
|
||||||
getLearnedSkillsDir,
|
getLearnedSkillsDir,
|
||||||
@@ -27,6 +28,9 @@ const { detectProjectType } = require('../lib/project-detect');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const INSTINCT_CONFIDENCE_THRESHOLD = 0.7;
|
||||||
|
const MAX_INJECTED_INSTINCTS = 6;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a filesystem path to its canonical (real) form.
|
* Resolve a filesystem path to its canonical (real) form.
|
||||||
*
|
*
|
||||||
@@ -155,10 +159,151 @@ function selectMatchingSession(sessions, cwd, currentProject) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseInstinctFile(content) {
|
||||||
|
const instincts = [];
|
||||||
|
let current = null;
|
||||||
|
let inFrontmatter = false;
|
||||||
|
let contentLines = [];
|
||||||
|
|
||||||
|
for (const line of String(content).split('\n')) {
|
||||||
|
if (line.trim() === '---') {
|
||||||
|
if (inFrontmatter) {
|
||||||
|
inFrontmatter = false;
|
||||||
|
} else {
|
||||||
|
if (current && current.id) {
|
||||||
|
current.content = contentLines.join('\n').trim();
|
||||||
|
instincts.push(current);
|
||||||
|
}
|
||||||
|
current = {};
|
||||||
|
contentLines = [];
|
||||||
|
inFrontmatter = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inFrontmatter) {
|
||||||
|
const separatorIndex = line.indexOf(':');
|
||||||
|
if (separatorIndex === -1) continue;
|
||||||
|
const key = line.slice(0, separatorIndex).trim();
|
||||||
|
let value = line.slice(separatorIndex + 1).trim();
|
||||||
|
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
||||||
|
value = value.slice(1, -1);
|
||||||
|
}
|
||||||
|
if (key === 'confidence') {
|
||||||
|
const parsed = Number.parseFloat(value);
|
||||||
|
current[key] = Number.isFinite(parsed) ? parsed : 0.5;
|
||||||
|
} else {
|
||||||
|
current[key] = value;
|
||||||
|
}
|
||||||
|
} else if (current) {
|
||||||
|
contentLines.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current && current.id) {
|
||||||
|
current.content = contentLines.join('\n').trim();
|
||||||
|
instincts.push(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return instincts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readInstinctsFromDir(directory, scope) {
|
||||||
|
if (!directory || !fs.existsSync(directory)) return [];
|
||||||
|
|
||||||
|
const entries = fs.readdirSync(directory, { withFileTypes: true })
|
||||||
|
.filter(entry => entry.isFile() && /\.(ya?ml|md)$/i.test(entry.name))
|
||||||
|
.sort((left, right) => left.name.localeCompare(right.name));
|
||||||
|
|
||||||
|
const instincts = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
const filePath = path.join(directory, entry.name);
|
||||||
|
try {
|
||||||
|
const parsed = parseInstinctFile(fs.readFileSync(filePath, 'utf8'));
|
||||||
|
for (const instinct of parsed) {
|
||||||
|
instincts.push({
|
||||||
|
...instinct,
|
||||||
|
_scopeLabel: scope,
|
||||||
|
_sourceFile: filePath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log(`[SessionStart] Warning: failed to parse instinct file ${filePath}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return instincts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractInstinctAction(content) {
|
||||||
|
const actionMatch = String(content || '').match(/## Action\s*\n+([\s\S]+?)(?:\n## |\n---|$)/);
|
||||||
|
const actionBlock = (actionMatch ? actionMatch[1] : String(content || '')).trim();
|
||||||
|
const firstLine = actionBlock
|
||||||
|
.split('\n')
|
||||||
|
.map(line => line.trim())
|
||||||
|
.find(Boolean);
|
||||||
|
|
||||||
|
return firstLine || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeActiveInstincts(observerContext) {
|
||||||
|
const homunculusDir = path.join(getClaudeDir(), 'homunculus');
|
||||||
|
const globalDirs = [
|
||||||
|
{ dir: path.join(homunculusDir, 'instincts', 'personal'), scope: 'global' },
|
||||||
|
{ dir: path.join(homunculusDir, 'instincts', 'inherited'), scope: 'global' },
|
||||||
|
];
|
||||||
|
const projectDirs = observerContext.isGlobal ? [] : [
|
||||||
|
{ dir: path.join(observerContext.projectDir, 'instincts', 'personal'), scope: 'project' },
|
||||||
|
{ dir: path.join(observerContext.projectDir, 'instincts', 'inherited'), scope: 'project' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const scopedInstincts = [
|
||||||
|
...projectDirs.flatMap(({ dir, scope }) => readInstinctsFromDir(dir, scope)),
|
||||||
|
...globalDirs.flatMap(({ dir, scope }) => readInstinctsFromDir(dir, scope)),
|
||||||
|
];
|
||||||
|
|
||||||
|
const deduped = new Map();
|
||||||
|
for (const instinct of scopedInstincts) {
|
||||||
|
if (!instinct.id || instinct.confidence < INSTINCT_CONFIDENCE_THRESHOLD) continue;
|
||||||
|
const existing = deduped.get(instinct.id);
|
||||||
|
if (!existing || (existing._scopeLabel !== 'project' && instinct._scopeLabel === 'project')) {
|
||||||
|
deduped.set(instinct.id, instinct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ranked = Array.from(deduped.values())
|
||||||
|
.map(instinct => ({
|
||||||
|
...instinct,
|
||||||
|
action: extractInstinctAction(instinct.content),
|
||||||
|
}))
|
||||||
|
.filter(instinct => instinct.action)
|
||||||
|
.sort((left, right) => {
|
||||||
|
if (right.confidence !== left.confidence) return right.confidence - left.confidence;
|
||||||
|
if (left._scopeLabel !== right._scopeLabel) return left._scopeLabel === 'project' ? -1 : 1;
|
||||||
|
return String(left.id).localeCompare(String(right.id));
|
||||||
|
})
|
||||||
|
.slice(0, MAX_INJECTED_INSTINCTS);
|
||||||
|
|
||||||
|
if (ranked.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`[SessionStart] Injecting ${ranked.length} instinct(s) into session context`);
|
||||||
|
|
||||||
|
const lines = ranked.map(instinct => {
|
||||||
|
const scope = instinct._scopeLabel === 'project' ? 'project' : 'global';
|
||||||
|
const confidence = `${Math.round(instinct.confidence * 100)}%`;
|
||||||
|
return `- [${scope} ${confidence}] ${instinct.action}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return `Active instincts:\n${lines.join('\n')}`;
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const sessionsDir = getSessionsDir();
|
const sessionsDir = getSessionsDir();
|
||||||
const learnedDir = getLearnedSkillsDir();
|
const learnedDir = getLearnedSkillsDir();
|
||||||
const additionalContextParts = [];
|
const additionalContextParts = [];
|
||||||
|
const observerContext = resolveProjectContext();
|
||||||
|
|
||||||
// Ensure directories exist
|
// Ensure directories exist
|
||||||
ensureDir(sessionsDir);
|
ensureDir(sessionsDir);
|
||||||
@@ -166,7 +311,6 @@ async function main() {
|
|||||||
|
|
||||||
const observerSessionId = resolveSessionId();
|
const observerSessionId = resolveSessionId();
|
||||||
if (observerSessionId) {
|
if (observerSessionId) {
|
||||||
const observerContext = resolveProjectContext();
|
|
||||||
writeSessionLease(observerContext, observerSessionId, {
|
writeSessionLease(observerContext, observerSessionId, {
|
||||||
hook: 'SessionStart',
|
hook: 'SessionStart',
|
||||||
projectRoot: observerContext.projectRoot
|
projectRoot: observerContext.projectRoot
|
||||||
@@ -176,6 +320,11 @@ async function main() {
|
|||||||
log('[SessionStart] No CLAUDE_SESSION_ID available; skipping observer lease registration');
|
log('[SessionStart] No CLAUDE_SESSION_ID available; skipping observer lease registration');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const instinctSummary = summarizeActiveInstincts(observerContext);
|
||||||
|
if (instinctSummary) {
|
||||||
|
additionalContextParts.push(instinctSummary);
|
||||||
|
}
|
||||||
|
|
||||||
// Check for recent session files (last 7 days)
|
// Check for recent session files (last 7 days)
|
||||||
const recentSessions = dedupeRecentSessions(getSessionSearchDirs());
|
const recentSessions = dedupeRecentSessions(getSessionSearchDirs());
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
const crypto = require('crypto');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
@@ -179,6 +180,26 @@ function cleanupTestDir(testDir) {
|
|||||||
fs.rmSync(testDir, { recursive: true, force: true });
|
fs.rmSync(testDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function writeInstinctFile(filePath, entries) {
|
||||||
|
const body = entries.map(entry => `---
|
||||||
|
id: ${entry.id}
|
||||||
|
trigger: "${entry.trigger}"
|
||||||
|
confidence: ${entry.confidence}
|
||||||
|
domain: ${entry.domain || 'general'}
|
||||||
|
scope: ${entry.scope}
|
||||||
|
---
|
||||||
|
|
||||||
|
## Action
|
||||||
|
${entry.action}
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
${entry.evidence || 'Learned from repeated observations.'}
|
||||||
|
`).join('\n');
|
||||||
|
|
||||||
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||||
|
fs.writeFileSync(filePath, body);
|
||||||
|
}
|
||||||
|
|
||||||
function getHookCommandByDescription(hooks, lifecycle, descriptionText) {
|
function getHookCommandByDescription(hooks, lifecycle, descriptionText) {
|
||||||
const hookGroup = hooks.hooks[lifecycle]?.find(
|
const hookGroup = hooks.hooks[lifecycle]?.find(
|
||||||
entry => entry.description && entry.description.includes(descriptionText)
|
entry => entry.description && entry.description.includes(descriptionText)
|
||||||
@@ -333,6 +354,66 @@ async function runTests() {
|
|||||||
}
|
}
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (await asyncTest('session-start injects high-confidence instincts into additionalContext', async () => {
|
||||||
|
const testDir = createTestDir();
|
||||||
|
const projectDir = path.join(testDir, 'project');
|
||||||
|
fs.mkdirSync(projectDir, { recursive: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const projectId = crypto.createHash('sha256').update(projectDir).digest('hex').slice(0, 12);
|
||||||
|
const homunculusDir = path.join(testDir, '.claude', 'homunculus');
|
||||||
|
const projectInstinctDir = path.join(homunculusDir, 'projects', projectId, 'instincts', 'personal');
|
||||||
|
const globalInstinctDir = path.join(homunculusDir, 'instincts', 'inherited');
|
||||||
|
|
||||||
|
writeInstinctFile(path.join(projectInstinctDir, 'project-instincts.yaml'), [
|
||||||
|
{
|
||||||
|
id: 'project-tests-first',
|
||||||
|
trigger: 'when changing tests',
|
||||||
|
confidence: 0.9,
|
||||||
|
scope: 'project',
|
||||||
|
action: 'Run the targeted *.test.js file first, then widen to node tests/run-all.js.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'project-low-confidence',
|
||||||
|
trigger: 'when guessing',
|
||||||
|
confidence: 0.4,
|
||||||
|
scope: 'project',
|
||||||
|
action: 'This should never be injected.',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
writeInstinctFile(path.join(globalInstinctDir, 'global-instincts.yaml'), [
|
||||||
|
{
|
||||||
|
id: 'global-validation',
|
||||||
|
trigger: 'when editing hooks',
|
||||||
|
confidence: 0.82,
|
||||||
|
scope: 'global',
|
||||||
|
action: 'Keep hook scripts, tests, and docs aligned in the same change set.',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await runHookWithInput(
|
||||||
|
path.join(scriptsDir, 'session-start.js'),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
HOME: testDir,
|
||||||
|
CLAUDE_PROJECT_DIR: projectDir,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(result.code, 0, 'SessionStart should exit 0');
|
||||||
|
const payload = getSessionStartPayload(result.stdout);
|
||||||
|
const additionalContext = payload.hookSpecificOutput.additionalContext;
|
||||||
|
|
||||||
|
assert.ok(additionalContext.includes('Active instincts:'), 'Should inject instinct summary into additionalContext');
|
||||||
|
assert.ok(additionalContext.includes('[project 90%] Run the targeted *.test.js file first, then widen to node tests/run-all.js.'), 'Should include project-scoped instinct');
|
||||||
|
assert.ok(additionalContext.includes('[global 82%] Keep hook scripts, tests, and docs aligned in the same change set.'), 'Should include global instinct');
|
||||||
|
assert.ok(!additionalContext.includes('This should never be injected.'), 'Should exclude low-confidence instincts');
|
||||||
|
} finally {
|
||||||
|
cleanupTestDir(testDir);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (await asyncTest('session-end-marker removes the last lease and stops the observer process', async () => {
|
if (await asyncTest('session-end-marker removes the last lease and stops the observer process', async () => {
|
||||||
const testDir = createTestDir();
|
const testDir = createTestDir();
|
||||||
const projectDir = path.join(testDir, 'project');
|
const projectDir = path.join(testDir, 'project');
|
||||||
|
|||||||
Reference in New Issue
Block a user