fix(coding-agent): keep skills available with bash-only tools (#8552)

This commit is contained in:
Alexey Zaytsev 2026-09-03 06:08:26 -03:00 • committed by GitHub
parent 6f35de5b59
commit 1d6dbf9e3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 54 additions and 12 deletions

View file

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

View file

@ -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.",
"",
"<available_skills>",

View file

@ -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 += "</project_context>\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 += "</project_context>\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}`;

View file

@ -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("<available_skills>");
expect(prompt).toContain("<name>test-skill</name>");
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("<available_skills>");
});
});
});