diff --git a/CHANGELOG.md b/CHANGELOG.md index d6d3555fe..b4e722071 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ - Config: support inline env vars in config (`env.*` / `env.vars`) and document env precedence. - Agent: enable adaptive context pruning by default for tool-result trimming. - Agent: drop empty error assistant messages when sanitizing session history. (#591) — thanks @steipete +- Agent: inject eligible skills list into the system prompt so bundled skills load from their actual locations. (#551) — thanks @gabriel-trigo - Doctor: check config/state permissions and offer to tighten them. — thanks @steipete - Doctor/Daemon: audit supervisor configs, add --repair/--force flows, surface service config audits in daemon status, and document user vs system services. — thanks @steipete - Doctor: repair gateway service entrypoint when switching between npm and git installs; add Docker e2e coverage. — thanks @steipete diff --git a/docs/concepts/system-prompt.md b/docs/concepts/system-prompt.md index 271204d3c..484b08a17 100644 --- a/docs/concepts/system-prompt.md +++ b/docs/concepts/system-prompt.md @@ -49,10 +49,19 @@ Use `agents.defaults.userTimezone` in `~/.clawdbot/clawdbot.json` to change the ## Skills -Skills are **not** auto-injected. Instead, the prompt instructs the model to use `read` to load skill instructions on demand: +When eligible skills exist, Clawdbot injects a compact **available skills list** +(`formatSkillsForPrompt`) that includes the **file path** for each skill. The +prompt instructs the model to use `read` to load the SKILL.md at the listed +location (workspace, managed, or bundled). ``` -/skills//SKILL.md + + + ... + ... + ... + + ``` This keeps the base prompt small while still enabling targeted skill usage. diff --git a/src/agents/pi-embedded-runner.test.ts b/src/agents/pi-embedded-runner.test.ts index fb102092e..70085ce0b 100644 --- a/src/agents/pi-embedded-runner.test.ts +++ b/src/agents/pi-embedded-runner.test.ts @@ -6,9 +6,11 @@ import { applyGoogleTurnOrderingFix, buildEmbeddedSandboxInfo, createSystemPromptOverride, + resolveSkillsPrompt, splitSdkTools, } from "./pi-embedded-runner.js"; import type { SandboxContext } from "./sandbox.js"; +import type { SkillEntry } from "./skills.js"; describe("buildEmbeddedSandboxInfo", () => { it("returns undefined when sandbox is missing", () => { @@ -122,6 +124,35 @@ describe("createSystemPromptOverride", () => { }); }); +describe("resolveSkillsPrompt", () => { + it("prefers snapshot prompt when available", () => { + const prompt = resolveSkillsPrompt({ + skillsSnapshot: { prompt: "SNAPSHOT", skills: [] }, + workspaceDir: "/tmp/clawd", + }); + expect(prompt).toBe("SNAPSHOT"); + }); + + it("builds prompt from entries when snapshot is missing", () => { + const entry: SkillEntry = { + skill: { + name: "demo-skill", + description: "Demo", + filePath: "/app/skills/demo-skill/SKILL.md", + baseDir: "/app/skills/demo-skill", + source: "clawdbot-bundled", + }, + frontmatter: {}, + }; + const prompt = resolveSkillsPrompt({ + skillEntries: [entry], + workspaceDir: "/tmp/clawd", + }); + expect(prompt).toContain(""); + expect(prompt).toContain("/app/skills/demo-skill/SKILL.md"); + }); +}); + describe("applyGoogleTurnOrderingFix", () => { const makeAssistantFirst = () => [ diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts index 1e5d80b67..6f494956b 100644 --- a/src/agents/pi-embedded-runner.ts +++ b/src/agents/pi-embedded-runner.ts @@ -89,7 +89,9 @@ import { resolveSandboxContext } from "./sandbox.js"; import { applySkillEnvOverrides, applySkillEnvOverridesFromSnapshot, + buildWorkspaceSkillsPrompt, loadWorkspaceSkillEntries, + type SkillEntry, type SkillSnapshot, } from "./skills.js"; import { buildAgentSystemPrompt } from "./system-prompt.js"; @@ -578,6 +580,7 @@ function buildEmbeddedSystemPrompt(params: { ownerNumbers?: string[]; reasoningTagHint: boolean; heartbeatPrompt?: string; + skillsPrompt?: string; runtimeInfo: { host: string; os: string; @@ -601,6 +604,7 @@ function buildEmbeddedSystemPrompt(params: { ownerNumbers: params.ownerNumbers, reasoningTagHint: params.reasoningTagHint, heartbeatPrompt: params.heartbeatPrompt, + skillsPrompt: params.skillsPrompt, runtimeInfo: params.runtimeInfo, sandboxInfo: params.sandboxInfo, toolNames: params.tools.map((tool) => tool.name), @@ -618,6 +622,24 @@ export function createSystemPromptOverride( return () => trimmed; } +export function resolveSkillsPrompt(params: { + skillsSnapshot?: SkillSnapshot; + skillEntries?: SkillEntry[]; + config?: ClawdbotConfig; + workspaceDir: string; +}): string { + const snapshotPrompt = params.skillsSnapshot?.prompt?.trim(); + if (snapshotPrompt) return snapshotPrompt; + if (params.skillEntries && params.skillEntries.length > 0) { + const prompt = buildWorkspaceSkillsPrompt(params.workspaceDir, { + entries: params.skillEntries, + config: params.config, + }); + return prompt.trim() ? prompt : ""; + } + return ""; +} + // Tool names are now capitalized (Bash, Read, Write, Edit) to bypass Anthropic's // OAuth token blocking of lowercase names. However, pi-coding-agent's SDK has // hardcoded lowercase names in its built-in tool registry, so we must pass ALL @@ -844,6 +866,12 @@ export async function compactEmbeddedPiSession(params: { skills: skillEntries ?? [], config: params.config, }); + const skillsPrompt = resolveSkillsPrompt({ + skillsSnapshot: params.skillsSnapshot, + skillEntries: shouldLoadSkillEntries ? skillEntries : undefined, + config: params.config, + workspaceDir: effectiveWorkspace, + }); const bootstrapFiles = await loadWorkspaceBootstrapFiles(effectiveWorkspace); @@ -895,6 +923,7 @@ export async function compactEmbeddedPiSession(params: { heartbeatPrompt: resolveHeartbeatPrompt( params.config?.agents?.defaults?.heartbeat?.prompt, ), + skillsPrompt, runtimeInfo, sandboxInfo, tools, @@ -1167,6 +1196,12 @@ export async function runEmbeddedPiAgent(params: { skills: skillEntries ?? [], config: params.config, }); + const skillsPrompt = resolveSkillsPrompt({ + skillsSnapshot: params.skillsSnapshot, + skillEntries: shouldLoadSkillEntries ? skillEntries : undefined, + config: params.config, + workspaceDir: effectiveWorkspace, + }); const bootstrapFiles = await loadWorkspaceBootstrapFiles(effectiveWorkspace); @@ -1208,6 +1243,7 @@ export async function runEmbeddedPiAgent(params: { heartbeatPrompt: resolveHeartbeatPrompt( params.config?.agents?.defaults?.heartbeat?.prompt, ), + skillsPrompt, runtimeInfo, sandboxInfo, tools, diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index b2e813c90..37acde766 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -90,10 +90,21 @@ describe("buildAgentSystemPrompt", () => { expect(prompt).toContain("## Skills"); expect(prompt).toContain( - "Use `read` to load from /tmp/clawd/skills//SKILL.md", + "Use `read` to load the SKILL.md at the location listed for that skill.", ); }); + it("appends available skills when provided", () => { + const prompt = buildAgentSystemPrompt({ + workspaceDir: "/tmp/clawd", + skillsPrompt: + "\n \n demo\n \n", + }); + + expect(prompt).toContain(""); + expect(prompt).toContain("demo"); + }); + it("renders project context files when provided", () => { const prompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/clawd", diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index abc01ac2a..9ff3a069e 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -12,6 +12,7 @@ export function buildAgentSystemPrompt(params: { userTimezone?: string; userTime?: string; contextFiles?: EmbeddedContextFile[]; + skillsPrompt?: string; heartbeatPrompt?: string; runtimeInfo?: { host?: string; @@ -121,6 +122,7 @@ export function buildAgentSystemPrompt(params: { : undefined; const userTimezone = params.userTimezone?.trim(); const userTime = params.userTime?.trim(); + const skillsPrompt = params.skillsPrompt?.trim(); const heartbeatPrompt = params.heartbeatPrompt?.trim(); const heartbeatPromptLine = heartbeatPrompt ? `Heartbeat prompt: ${heartbeatPrompt}` @@ -136,6 +138,7 @@ export function buildAgentSystemPrompt(params: { const telegramInlineButtonsEnabled = runtimeProvider === "telegram" && runtimeCapabilitiesLower.has("inlinebuttons"); + const skillsLines = skillsPrompt ? [skillsPrompt, ""] : []; const lines = [ "You are a personal assistant running inside Clawdbot.", @@ -164,7 +167,8 @@ export function buildAgentSystemPrompt(params: { "If a task is more complex or takes longer, spawn a sub-agent. It will do the work for you and ping you when it's done. You can always check up on it.", "", "## Skills", - `Skills provide task-specific instructions. Use \`read\` to load from ${params.workspaceDir}/skills//SKILL.md when needed.`, + "Skills provide task-specific instructions. Use `read` to load the SKILL.md at the location listed for that skill.", + ...skillsLines, "", hasGateway ? "## Clawdbot Self-Update" : "", hasGateway