fix: inject skills prompt list

This commit is contained in:
Peter Steinberger
2026-01-09 21:20:38 +01:00
parent 0297b38ce0
commit 4861f09f78
6 changed files with 96 additions and 4 deletions

View File

@@ -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

View File

@@ -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).
```
<workspace>/skills/<name>/SKILL.md
<available_skills>
<skill>
<name>...</name>
<description>...</description>
<location>...</location>
</skill>
</available_skills>
```
This keeps the base prompt small while still enabling targeted skill usage.

View File

@@ -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("<available_skills>");
expect(prompt).toContain("/app/skills/demo-skill/SKILL.md");
});
});
describe("applyGoogleTurnOrderingFix", () => {
const makeAssistantFirst = () =>
[

View File

@@ -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,

View File

@@ -90,10 +90,21 @@ describe("buildAgentSystemPrompt", () => {
expect(prompt).toContain("## Skills");
expect(prompt).toContain(
"Use `read` to load from /tmp/clawd/skills/<name>/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:
"<available_skills>\n <skill>\n <name>demo</name>\n </skill>\n</available_skills>",
});
expect(prompt).toContain("<available_skills>");
expect(prompt).toContain("<name>demo</name>");
});
it("renders project context files when provided", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/clawd",

View File

@@ -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/<name>/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