Skip to content
305 changes: 303 additions & 2 deletions bin/commands.js

Large diffs are not rendered by default.

61 changes: 61 additions & 0 deletions bin/executor.js
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,19 @@ async function executeTool(name, args, ctx) {
const objects = Array.isArray(raw) ? raw : (raw?.objects || []);
const tokens_used = Array.isArray(raw) ? objects.length * 50 : (raw?.tokens_used || 0);
if (objects.length === 0) return { result: 'No relevant memory found.' };
// Touch last_used_at so hygiene tier sweeps see real usage — an
// actively-retrieved entry must not age out. Never breaks retrieval.
for (const o of objects) {
try {
const now = new Date().toISOString();
if (typeof memoryStore.update === 'function') {
memoryStore.update(o.id, { last_used_at: now });
} else {
o.last_used_at = now;
if (typeof memoryStore.save === 'function') memoryStore.save();
}
} catch {}
}
const formatted = objects.map(o => `[${o.type}] ${o.title}: ${o.content}`).join('\n\n');
return { result: `Loaded ${objects.length} memories (${tokens_used} tokens):\n\n${formatted}` };
}
Expand Down Expand Up @@ -840,6 +853,54 @@ async function executeTool(name, args, ctx) {
return { result: '' };
}

case 'use_skill': {
const skillManager = ctx.skillManager || null;
if (!skillManager) return { error: 'use_skill: skill system not available' };
const skillName = String(args.name || '').trim();
if (!skillName) return { error: 'use_skill: name is required' };
const skill = skillManager.get(skillName);
if (!skill) {
const validNames = skillManager.getIndex().map(e => e.name).slice(0, 10);
return { error: `use_skill: skill "${skillName}" not found. Valid names: ${validNames.join(', ')}` };
}
const { formatSkillResult } = require('../src/plugins/skill_index_formatter');
const index = skillManager.getIndex();
const relatedEntries = (skill.related || [])
.map(r => index.find(e => e.name === r))
.filter(Boolean);
return { result: formatSkillResult(skill, relatedEntries) };
}

case 'spawn_agent': {
const agentName = String(args.agent || '').trim();
const agentTask = String(args.task || '').trim();
if (!agentName) return { error: 'spawn_agent: agent name is required' };
if (!agentTask) return { error: 'spawn_agent: task is required' };

try {
const { AgentLoader } = require('../src/plugins/agent_loader');
const { AgentRunner } = require('../src/plugins/agent_runner');
const loader = new AgentLoader(cwd);
const agentDef = loader.get(agentName);
if (!agentDef) {
const valid = loader.list().map(a => a.name);
return { error: `spawn_agent: agent "${agentName}" not found. Valid agents: ${valid.join(', ') || '(none defined)'}` };
}
const agentCtx = {
config,
flags: flags || {},
tui: tui || { renderDiff: () => null },
skillManager: ctx.skillManager || null,
};
const runner = new AgentRunner(agentDef, agentCtx);
const result = await runner.run(agentTask);
const summary = `[${agentName}] steps=${result.steps} tokens=${result.tokens}${result.error ? ' error=' + result.error : ''}`;
return { result: result.output ? `${summary}\n\n${result.output}` : summary };
} catch (e) {
return { error: `spawn_agent: ${e.message}` };
}
}

case 'bone_compile': {
const safe = safeResolvePath(args.path, cwd);
if (!safe.ok) return { error: `bone_compile rejected: ${safe.reason}` };
Expand Down
5 changes: 4 additions & 1 deletion bin/memory.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const INDEX_FILE = '.smallcode/memory/index.json';
// ─── Memory Object ───────────────────────────────────────────────────────────

class MemoryObject {
constructor({ id, type, title, content, tags, relations, createdAt, updatedAt, source }) {
constructor({ id, type, title, content, tags, relations, createdAt, updatedAt, source, tier, last_used_at }) {
this.id = id || crypto.randomUUID().slice(0, 8);
this.type = type; // decision | workflow | gotcha | convention | context | source
this.title = title;
Expand All @@ -30,13 +30,16 @@ class MemoryObject {
this.createdAt = createdAt || new Date().toISOString();
this.updatedAt = updatedAt || new Date().toISOString();
this.source = source || null; // { file, line, commit }
this.tier = tier || 'hot'; // hot | archive
this.last_used_at = last_used_at || this.createdAt;
}

toJSON() {
return {
id: this.id, type: this.type, title: this.title, content: this.content,
tags: this.tags, relations: this.relations,
createdAt: this.createdAt, updatedAt: this.updatedAt, source: this.source,
tier: this.tier, last_used_at: this.last_used_at,
};
}
}
Expand Down
28 changes: 20 additions & 8 deletions bin/smallcode.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ async function runTUI(config) {
onCommand: async (cmd) => {
if (cmd === '/quit' || cmd === '/q' || cmd === '/exit') {
if (sessionStore) sessionStore.save(conversationHistory, { tokens: tokenTracker ? tokenTracker.stats() : undefined });
try { if (memoryStore) { const { runHygiene } = require('../src/memory/hygiene'); runHygiene(memoryStore); } } catch {}
screen.leave();
killMCP()
process.exit(0);
Expand Down Expand Up @@ -318,6 +319,7 @@ async function runTUI(config) {
if (sessionStore) {
sessionStore.save(conversationHistory, { tokens: tokenTracker ? tokenTracker.stats() : undefined });
}
try { if (memoryStore) { const { runHygiene } = require('../src/memory/hygiene'); runHygiene(memoryStore); } } catch {}
killMCP()
process.exit(0);
},
Expand Down Expand Up @@ -444,6 +446,7 @@ async function executeTool(name, args) {
flags,
config,
tui,
skillManager,
});

try { if (dedup) dedup.record(name, args, result); } catch {}
Expand Down Expand Up @@ -2086,21 +2089,29 @@ function getMemoryContext(messages) {
}
}

// Auto-load relevant skills based on the user's message
// Auto-load relevant skills based on the user's message.
// Fix #18: Cap skill injection to ~1000 tokens (4000 chars). Multiple matching
// skills can each be a full .md file, quickly blowing up the system prompt.
//
// Lazy-skills: always inject the compact index (one line per skill, ~8 tokens each)
// so the model can call use_skill to pull any body on demand. Auto-matched skill
// bodies are appended after the index, subject to the 4000-char aggregate cap.
function getSkillContext(messages) {
if (!skillManager) return '';
try {
const { formatSkillIndex } = require('../src/plugins/skill_index_formatter');
const index = skillManager.getIndex();
const indexStr = formatSkillIndex(index);

const lastUser = [...messages].reverse().find(m => m.role === 'user');
if (!lastUser) return '';
const skills = skillManager.getAutoSkills(lastUser.content);
if (skills.length === 0) return '';
const formatted = skillManager.formatForPrompt(skills);
const autoSkills = lastUser ? skillManager.getAutoSkills(lastUser.content) : [];
const autoFormatted = skillManager.formatForPrompt(autoSkills);

const combined = indexStr + (autoFormatted ? '\n' + autoFormatted : '');
// Hard cap: truncate if too long
return formatted.length > 4000
? formatted.slice(0, 4000) + '\n... (skills truncated to fit context)'
: formatted;
return combined.length > 4000
? combined.slice(0, 4000) + '\n... (skills truncated to fit context)'
: combined;
} catch {
return '';
}
Expand Down Expand Up @@ -2546,6 +2557,7 @@ async function chatCompletion(config, messages) {
});
sessionStore.autoTitle(conversationHistory);
}
try { if (memoryStore) { const { runHygiene } = require('../src/memory/hygiene'); runHygiene(memoryStore); } } catch {}

return data;
} catch (err) {
Expand Down
2 changes: 2 additions & 0 deletions bin/tools.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 11 additions & 8 deletions src/compiled/tool_router.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,25 +228,28 @@ function classifyToolCategory(message) {
* @returns {string[]} tool names to include in the prompt
*/
function getToolsForCategory(category) {
// use_skill rides along in every tool-bearing category — the skill index
// is injected on every turn, so the model must always be able to pull a
// skill body regardless of how the task was classified (~80 token cost).
switch (category) {
case 'code_intel':
return ['graph_search', 'explain_symbol', 'read_file', 'find_files', 'search', 'hybrid_search'];
return ['graph_search', 'explain_symbol', 'read_file', 'find_files', 'search', 'hybrid_search', 'use_skill'];
case 'read':
return ['read_file', 'list_projects', 'graph_search', 'find_files', 'find_and_read'];
return ['read_file', 'list_projects', 'graph_search', 'find_files', 'find_and_read', 'use_skill'];
case 'write':
return ['read_file', 'write_file', 'patch', 'bash', 'read_and_patch', 'create_and_run'];
return ['read_file', 'write_file', 'patch', 'bash', 'read_and_patch', 'create_and_run', 'use_skill', 'spawn_agent'];
case 'search':
return ['search', 'find_files', 'graph_search', 'read_file', 'explain_symbol', 'search_and_read', 'hybrid_search'];
return ['search', 'find_files', 'graph_search', 'read_file', 'explain_symbol', 'search_and_read', 'hybrid_search', 'use_skill'];
case 'run':
return ['bash', 'run', 'read_file'];
return ['bash', 'run', 'read_file', 'use_skill'];
case 'plan':
return ['read_file', 'write_file', 'patch', 'bash', 'search', 'find_files', 'graph_search', 'memory_load', 'memory_remember', 'bone_compile', 'bone_check', 'read_and_patch', 'create_and_run', 'find_and_read', 'search_and_read'];
return ['read_file', 'write_file', 'patch', 'bash', 'search', 'find_files', 'graph_search', 'memory_load', 'memory_remember', 'bone_compile', 'bone_check', 'read_and_patch', 'create_and_run', 'find_and_read', 'search_and_read', 'use_skill', 'spawn_agent'];
case 'web':
return ['web_search', 'web_fetch', 'read_file'];
return ['web_search', 'web_fetch', 'read_file', 'use_skill'];
case 'respond':
return []; // No tools needed for pure responses
default:
return ['read_file', 'write_file', 'patch', 'bash', 'search'];
return ['read_file', 'write_file', 'patch', 'bash', 'search', 'use_skill', 'spawn_agent'];
}
}

Expand Down
Loading