mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(coding-agent): keep skills available with bash-only tools (#8552)
This commit is contained in:
parent
6f35de5b59
commit
1d6dbf9e3d
4 changed files with 54 additions and 12 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>",
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
|
|
|
|||
|
|
@ -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>");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue