Skip to content

Commit e048781

Browse files
ndemiancclaude
andcommitted
Atom++ AI: implicit skills (M6.5) + /skills command
SKILL.md registry (skills.js, dependency-free) + 3 bundled skills (code-review, focused-fix, test-writing). The agent self-selects via a use_skill tool (name+description in the system prompt, body fetched on demand); /skills lists them. Gated on atompp.ai.skills.enabled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b340a1b commit e048781

8 files changed

Lines changed: 299 additions & 5 deletions

File tree

‎extensions/atom-ai/agent.js‎

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const cp = require('child_process');
1616
const { streamClaudeAgentTurn } = require('./providers');
1717
const { formatVerifyFeedback, verifyOutcome, looksUnrunnable, sniffPort, looksReady } = require('./verify');
1818

19-
const SYSTEM = [
19+
const SYSTEM_BASE = [
2020
"You are Atom++'s built-in autonomous coding agent. You accomplish the user's goal in their",
2121
'workspace using the provided tools. Rules:',
2222
'- Be DECISIVE and FAST. Read only the file you are changing (plus at most 1 other if truly needed), then ACT. Do NOT write long multi-point plans — at most ONE short sentence before each tool call.',
@@ -29,6 +29,7 @@ const SYSTEM = [
2929
'- Paths are relative to the workspace root.',
3030
'- For a multi-step goal, call update_plan FIRST with a short checklist (3-8 short items, all "pending"), then call it again to set an item "in_progress" when you start it and "done" when finished. Skip the plan for trivial single-step goals.',
3131
'- If the goal truly depends on a decision only the user can make (tech stack, scope, where to create files, must-have features), call ask_user ONCE with concise multiple-choice questions (a short header + 2-4 concrete options each) INSTEAD of writing the questions as prose. Then act on their answers and do not ask again. Do NOT ask about things you can reasonably decide yourself — prefer a sensible default and proceed.',
32+
'- You have SKILLS — short expert playbooks for common task types, listed under "Available skills" below with a name and a one-line description. When the user\'s goal clearly matches a skill\'s description (e.g. reviewing a diff, fixing one reported bug, writing tests), call use_skill with that exact name FIRST, before other tools, and follow the steps it returns. Pick at most one; if nothing clearly fits, just proceed normally. Do not mention skills to the user.',
3233
'- When the goal is finished, end with a one-line summary starting with "Done:" and STOP (no more tools).',
3334
'- After you finish, your work is verified automatically: editor diagnostics for the files you changed (plus a verify command, if the user configured one) are checked. If problems are found you will get them back as a follow-up message — fix the ones you introduced, then finish again with "Done:". If a reported problem is clearly pre-existing and unrelated to the goal, do not chase it: note it in one line and finish.'
3435
].join('\n');
@@ -42,12 +43,21 @@ const TOOLS = [
4243
{ name: 'write_file', description: 'Create a new file (or fully overwrite a short one) with the COMPLETE content. For edits to existing files, prefer edit_file. Applied immediately; the user reviews it with Keep/Undo.', input_schema: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] } },
4344
{ name: 'run_command', description: 'Run a shell command in the workspace root. Requires approval. Pass background:true for commands that do not exit on their own (servers, watchers) so the agent is not blocked — it returns immediately and you read progress later with read_command_output.', input_schema: { type: 'object', properties: { command: { type: 'string' }, explanation: { type: 'string' }, background: { type: 'boolean', description: 'true = start it and keep working without waiting (dev servers, watchers, tail -f). Returns immediately with an id; poll read_command_output for its output/status.' } }, required: ['command'] } },
4445
{ name: 'read_command_output', description: 'Read recent output + status of a command started with run_command background:true. Returns a status header ([running on :3000] / [exited 0] / [stopped]) followed by the latest output lines. Poll this to wait for a server to become ready before testing against it.', input_schema: { type: 'object', properties: { id: { type: 'string', description: 'the id returned by a background run_command' }, lines: { type: 'number', description: 'max recent output lines to return (default 80, max 400)' } }, required: ['id'] } },
45-
{ name: 'ask_user', description: 'Ask the user one or more multiple-choice questions when the goal genuinely depends on a decision only they can make (tech stack, scope, where to put files, must-have features). The user picks by CLICKING — do NOT write questions as prose. Ask ONCE up front with all your questions, then proceed with the answers and never re-ask. Prefer sensible defaults over asking; only ask when a wrong guess would waste real work.', input_schema: { type: 'object', properties: { questions: { type: 'array', items: { type: 'object', properties: { header: { type: 'string', description: 'a 1-3 word tag for the question' }, question: { type: 'string' }, multiSelect: { type: 'boolean', description: 'true if several options can be picked at once' }, options: { type: 'array', items: { type: 'object', properties: { label: { type: 'string' }, description: { type: 'string' } }, required: ['label'] } } }, required: ['question', 'options'] } } }, required: ['questions'] } }
46+
{ name: 'ask_user', description: 'Ask the user one or more multiple-choice questions when the goal genuinely depends on a decision only they can make (tech stack, scope, where to put files, must-have features). The user picks by CLICKING — do NOT write questions as prose. Ask ONCE up front with all your questions, then proceed with the answers and never re-ask. Prefer sensible defaults over asking; only ask when a wrong guess would waste real work.', input_schema: { type: 'object', properties: { questions: { type: 'array', items: { type: 'object', properties: { header: { type: 'string', description: 'a 1-3 word tag for the question' }, question: { type: 'string' }, multiSelect: { type: 'boolean', description: 'true if several options can be picked at once' }, options: { type: 'array', items: { type: 'object', properties: { label: { type: 'string' }, description: { type: 'string' } }, required: ['label'] } } }, required: ['question', 'options'] } } }, required: ['questions'] } },
47+
{ name: 'use_skill', description: 'Load an expert playbook (SKILL.md) for a task type, chosen from the "Available skills" list in your system prompt. Returns the skill\'s step-by-step instructions as the tool result — then follow them. Read-only and instant (no approval). Call it once, early, when the goal matches a skill\'s description.', input_schema: { type: 'object', properties: { name: { type: 'string', description: 'the exact skill name from the Available skills list' } }, required: ['name'] } }
4648
];
4749

4850
// Rough token estimates (chars/4) for the static prompt segments, so the context popover can break
4951
// down "what's filling the window" — system + tools are sent on every request, the rest is messages.
50-
const SYSTEM_TOKENS_EST = Math.round(SYSTEM.length / 4);
52+
const SYSTEM_TOKENS_EST = Math.round(SYSTEM_BASE.length / 4);
53+
54+
/** Append an "Available skills" name+description menu to the base prompt. Bodies NEVER go here —
55+
* progressive disclosure means only name+description are ever in the prompt (~100 words for a dozen). */
56+
function buildSystem(menu) {
57+
if (!menu || !menu.length) { return SYSTEM_BASE; }
58+
const list = menu.map((s) => '- ' + s.name + ': ' + s.description).join('\n');
59+
return SYSTEM_BASE + '\n\nAvailable skills (call use_skill with the name):\n' + list;
60+
}
5161
const TOOLS_TOKENS_EST = Math.round(JSON.stringify(TOOLS).length / 4);
5262

5363
const FILE_EXCLUDES = '{**/node_modules/**,**/.git/**,**/out/**,**/dist/**,**/.vscode-test/**,**/*.map}';
@@ -343,6 +353,17 @@ async function runTool(tu, ctx) {
343353
if (res.notes) { lines.push('- Additional notes from the user: ' + res.notes); }
344354
return 'The user answered your questions (these are their recorded decisions — honor them and do NOT ask again):\n' + lines.join('\n') + '\n\nProceed with the work using these answers.';
345355
}
356+
if (tu.name === 'use_skill') {
357+
const name = String(input.name || '').trim();
358+
if (!ctx.skills) { return 'ERROR: skills are not available in this session. Proceed without one.'; }
359+
const body = ctx.skills.getBody(name);
360+
if (body == null) {
361+
const avail = ctx.skills.menu().map((s) => s.name).join(', ') || '(none)';
362+
return 'ERROR: no skill named "' + name + '". Available skills: ' + avail + '. Pick one exactly, or proceed without a skill.';
363+
}
364+
ctx.post({ type: 'agentTool', icon: 'sparkle', text: '🧩 using skill: ' + name }); // quiet chip — only on success (🧩 is the marker; 'sparkle' falls back cleanly)
365+
return body; // SKILL.md body → tool_result, steers the next turns
366+
}
346367
return 'ERROR: unknown tool ' + tu.name;
347368
} catch (e) {
348369
return 'ERROR: ' + ((e && e.message) || e);
@@ -362,6 +383,9 @@ async function runAgent(ctx) {
362383
const root = workspaceRoot();
363384
if (!root) { ctx.post({ type: 'agentError', message: 'Open a folder first — the agent works on your workspace.' }); ctx.post({ type: 'agentDone', reason: 'error' }); return; }
364385
ctx.root = root;
386+
// M6.5 implicit skills: build the system prompt ONCE per run — append the tiny name+description menu.
387+
const system = ctx.skills ? buildSystem(ctx.skills.menu()) : SYSTEM_BASE;
388+
const systemTokensEst = Math.round(system.length / 4);
365389

366390
const dbg = ctx.dbg || (() => {});
367391
const messages = ctx.messages;
@@ -437,7 +461,7 @@ async function runAgent(ctx) {
437461
let streamed = false;
438462
let textChars = 0;
439463
const turn = await streamClaudeAgentTurn({
440-
apiKey: ctx.apiKey, model: ctx.model, maxTokens: 8192, system: SYSTEM,
464+
apiKey: ctx.apiKey, model: ctx.model, maxTokens: 8192, system: system,
441465
messages, tools: TOOLS, signal: ctx.signal,
442466
onText: (t) => { streamed = true; textChars += t.length; ctx.post({ type: 'agentDelta', text: t }); },
443467
onToolStart: (name) => {
@@ -452,7 +476,7 @@ async function runAgent(ctx) {
452476
// Report real context usage (input_tokens = how full the transcript is) so the UI can meter + warn.
453477
if (turn.usage) {
454478
dbg('usage', { input: turn.usage.input_tokens, output: turn.usage.output_tokens, cacheRead: turn.usage.cache_read_input_tokens });
455-
ctx.post({ type: 'contextUsage', input: (turn.usage.input_tokens || 0) + (turn.usage.cache_read_input_tokens || 0) + (turn.usage.cache_creation_input_tokens || 0), output: turn.usage.output_tokens || 0, limit: ctx.contextLimit || 200000, model: ctx.model, system: SYSTEM_TOKENS_EST, tools: TOOLS_TOKENS_EST });
479+
ctx.post({ type: 'contextUsage', input: (turn.usage.input_tokens || 0) + (turn.usage.cache_read_input_tokens || 0) + (turn.usage.cache_creation_input_tokens || 0), output: turn.usage.output_tokens || 0, limit: ctx.contextLimit || 200000, model: ctx.model, system: systemTokensEst, tools: TOOLS_TOKENS_EST });
456480
}
457481

458482
// Guard: never push an empty assistant message — Anthropic 400s on content:[] (e.g. a

‎extensions/atom-ai/extension.js‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const { registerInlineComplete } = require('./inlineComplete');
1919
const { runAgent } = require('./agent');
2020
const { registerReview } = require('./reviewSession');
2121
const { formatDiagnosticLines, diagKey } = require('./verify');
22+
const { loadSkills, skillsMenu, getSkillBody } = require('./skills');
2223

2324
const CLAUDE_MODELS = [
2425
{ label: 'Claude Opus 4.8', id: 'claude-opus-4-8', detail: 'Most capable' },
@@ -565,6 +566,12 @@ async function agentFlow(text) {
565566
includeWarnings: cfg.get('verify.includeWarnings', false)
566567
};
567568
const runTouched = new Set(); // abs paths edited this run (agent.js fills it via ctx.touched)
569+
// M6.5 implicit skills: scan bundled SKILL.md once, expose a tiny menu()/getBody() resolver to the agent.
570+
let skillsObj = null;
571+
if (cfg.get('skills.enabled', true)) {
572+
const skillIndex = loadSkills(ctx.extensionPath, dbg);
573+
if (skillIndex.size) { skillsObj = { menu: () => skillsMenu(skillIndex), getBody: (name) => getSkillBody(skillIndex, name) }; }
574+
}
568575
const diagBaseline = verifyCfg.enabled ? snapshotDiagnostics() : new Map();
569576
dbg('verify.config', { enabled: verifyCfg.enabled, hasCommand: !!verifyCfg.command, maxRounds: verifyCfg.maxRounds, includeWarnings: verifyCfg.includeWarnings });
570577
try {
@@ -576,6 +583,7 @@ async function agentFlow(text) {
576583
post, dbg,
577584
approve: requestApproval, // run_command only
578585
ask: requestQuestions, // ask_user — clickable clarifying questions
586+
skills: skillsObj, // M6.5: implicit skills (name+desc menu in SYSTEM + use_skill resolver)
579587
contextLimit: cfg.get('contextWindow', 200000), // model context window → drives the usage meter
580588
commandStops: commandStops, // runId → stop() (process-group kill); used by Stop button / ■
581589
commandRuns: bgRuns, // runId → background-process registry (read_command_output reads it)
@@ -734,6 +742,7 @@ class ChatViewProvider {
734742
case 'retry': if (lastAgentGoal && !abort) { dbg('retry', { goalChars: lastAgentGoal.length }); await agentFlow(lastAgentGoal); } break;
735743
case 'continueAgent': if (!abort) { const g = lastAgentGoal; dbg('continue', { transcriptMsgs: agentMessages.length }); await agentFlow('Continue from where you left off and finish the task. Pick up exactly where you stopped — do not restart or repeat work that is already done.'); lastAgentGoal = g; } break;
736744
case 'restoreCheckpoint': dbg('restoreCheckpoint', { turnId: msg.turnId, running: !!abort }); await restoreCheckpoint(msg.turnId); break;
745+
case 'listSkills': { const en = aiConfig().get('skills.enabled', true); const idx = en ? loadSkills(ctx.extensionPath, dbg) : new Map(); dbg('listSkills', { enabled: en, count: idx.size }); post({ type: 'skillsList', enabled: en, skills: skillsMenu(idx) }); break; }
737746
case 'feedback': dbg('feedback', { value: msg.value }); break;
738747
case 'openFile': await openWorkspaceFile(msg.path); break;
739748
case 'reviewKeepFile': dbg('review.keep', { id: msg.id }); review.keepFile(msg.id, 'kept'); break;

0 commit comments

Comments
 (0)