From 1d6dbf9e3d60f129bf8ee513c82a98f222c57788 Mon Sep 17 00:00:00 2001 From: Alexey Zaytsev Date: Thu, 3 Sep 2026 06:08:26 -0300 Subject: [PATCH] fix(coding-agent): keep skills available with bash-only tools (#8552) --- packages/coding-agent/docs/skills.md | 2 +- packages/coding-agent/src/core/skills.ts | 6 ++- .../coding-agent/src/core/system-prompt.ts | 17 ++++---- .../coding-agent/test/system-prompt.test.ts | 41 +++++++++++++++++++ 4 files changed, 54 insertions(+), 12 deletions(-) diff --git a/packages/coding-agent/docs/skills.md b/packages/coding-agent/docs/skills.md index 8905b7ff9..02835f820 100644 --- a/packages/coding-agent/docs/skills.md +++ b/packages/coding-agent/docs/skills.md @@ -66,7 +66,7 @@ For project-level Claude Code skills, add to `.pi/settings.json`: 1. At startup, pi scans skill locations and extracts names and descriptions 2. The system prompt includes available skills in XML format per the [specification](https://agentskills.io/integrate-skills) -3. When a task matches, the agent uses `read` to load the full SKILL.md (models don't always do this; use prompting or `/skill:name` to force it) +3. When a task matches, the agent uses `read`, or `bash` when `read` is unavailable, to load the full SKILL.md (models don't always do this; use prompting or `/skill:name` to force it) 4. The agent follows the instructions, using relative paths to reference scripts and assets This is progressive disclosure: only descriptions are always in context, full instructions load on-demand. diff --git a/packages/coding-agent/src/core/skills.ts b/packages/coding-agent/src/core/skills.ts index 464129b64..744c1395a 100644 --- a/packages/coding-agent/src/core/skills.ts +++ b/packages/coding-agent/src/core/skills.ts @@ -352,7 +352,7 @@ function loadSkillFromFile( * Skills with disableModelInvocation=true are excluded from the prompt * (they can only be invoked explicitly via /skill:name commands). */ -export function formatSkillsForPrompt(skills: Skill[]): string { +export function formatSkillsForPrompt(skills: Skill[], fileReadTool: "read" | "bash" = "read"): string { const visibleSkills = skills.filter((s) => !s.disableModelInvocation); if (visibleSkills.length === 0) { @@ -361,7 +361,9 @@ export function formatSkillsForPrompt(skills: Skill[]): string { const lines = [ "\n\nThe following skills provide specialized instructions for specific tasks.", - "Use the read tool to load a skill's file when the task matches its description.", + fileReadTool === "read" + ? "Use the read tool to load a skill's file when the task matches its description." + : "Use bash to load a skill's file when the task matches its description.", "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.", "", "", diff --git a/packages/coding-agent/src/core/system-prompt.ts b/packages/coding-agent/src/core/system-prompt.ts index ec450678a..92476917a 100644 --- a/packages/coding-agent/src/core/system-prompt.ts +++ b/packages/coding-agent/src/core/system-prompt.ts @@ -42,6 +42,8 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions): string { const contextFiles = providedContextFiles ?? []; const skills = providedSkills ?? []; + const tools = selectedTools || ["read", "bash", "edit", "write"]; + const skillFileReadTool = (["read", "bash"] as const).find((tool) => tools.includes(tool)); if (customPrompt) { let prompt = customPrompt; @@ -60,10 +62,9 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions): string { prompt += "\n"; } - // Append skills section (only if read tool is available) - const customPromptHasRead = !selectedTools || selectedTools.includes("read"); - if (customPromptHasRead && skills.length > 0) { - prompt += formatSkillsForPrompt(skills); + // Append skills when a tool capable of reading their files is available. + if (skillFileReadTool && skills.length > 0) { + prompt += formatSkillsForPrompt(skills, skillFileReadTool); } prompt += `\nCurrent working directory: ${promptCwd}\n`; @@ -78,7 +79,6 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions): string { // Build tools list based on selected tools. // A tool appears in Available tools only when the caller provides a one-line snippet. - const tools = selectedTools || ["read", "bash", "edit", "write"]; const visibleTools = tools.filter((name) => !!toolSnippets?.[name]); const toolsList = visibleTools.length > 0 ? visibleTools.map((name) => `- ${name}: ${toolSnippets![name]}`).join("\n") : "(none)"; @@ -99,7 +99,6 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions): string { const hasGrep = tools.includes("grep"); const hasFind = tools.includes("find"); const hasLs = tools.includes("ls"); - const hasRead = tools.includes("read"); // File exploration guidelines if ((hasBash || hasPowerShell) && !hasGrep && !hasFind && !hasLs) { @@ -158,9 +157,9 @@ Pi documentation (read only when the user asks about pi itself, its SDK, extensi prompt += "\n"; } - // Append skills section (only if read tool is available) - if (hasRead && skills.length > 0) { - prompt += formatSkillsForPrompt(skills); + // Append skills when a tool capable of reading their files is available. + if (skillFileReadTool && skills.length > 0) { + prompt += formatSkillsForPrompt(skills, skillFileReadTool); } prompt += `\nCurrent working directory: ${promptCwd}`; diff --git a/packages/coding-agent/test/system-prompt.test.ts b/packages/coding-agent/test/system-prompt.test.ts index 41d83207b..9a943f116 100644 --- a/packages/coding-agent/test/system-prompt.test.ts +++ b/packages/coding-agent/test/system-prompt.test.ts @@ -1,6 +1,17 @@ import { describe, expect, test } from "vitest"; +import type { Skill } from "../src/core/skills.ts"; +import { createSyntheticSourceInfo } from "../src/core/source-info.ts"; import { buildSystemPrompt } from "../src/core/system-prompt.ts"; +const testSkill: Skill = { + name: "test-skill", + description: "A test skill.", + filePath: "/skills/test-skill/SKILL.md", + baseDir: "/skills/test-skill", + sourceInfo: createSyntheticSourceInfo("/skills/test-skill/SKILL.md", { source: "test" }), + disableModelInvocation: false, +}; + describe("buildSystemPrompt", () => { describe("empty tools", () => { test("shows (none) for empty tools list", () => { @@ -126,4 +137,34 @@ describe("buildSystemPrompt", () => { expect(prompt.match(/- Use dynamic_tool for summaries\./g)).toHaveLength(1); }); }); + + describe("skills", () => { + test.each([ + { name: "default prompt", customPrompt: undefined }, + { name: "custom prompt", customPrompt: "Custom system prompt" }, + ])("includes skills with only bash in the $name", ({ customPrompt }) => { + const prompt = buildSystemPrompt({ + customPrompt, + selectedTools: ["bash"], + contextFiles: [], + skills: [testSkill], + cwd: process.cwd(), + }); + + expect(prompt).toContain(""); + expect(prompt).toContain("test-skill"); + expect(prompt).toContain("Use bash to load a skill's file"); + }); + + test("omits skills without read or bash", () => { + const prompt = buildSystemPrompt({ + selectedTools: ["write"], + contextFiles: [], + skills: [testSkill], + cwd: process.cwd(), + }); + + expect(prompt).not.toContain(""); + }); + }); });