diff --git a/src/main/prompt-builder/system-prompt.ts b/src/main/prompt-builder/system-prompt.ts
index 89ac596d..8e81b2dc 100644
--- a/src/main/prompt-builder/system-prompt.ts
+++ b/src/main/prompt-builder/system-prompt.ts
@@ -1,6 +1,5 @@
import { DEPARTMENT_LABELS } from "@shared/constants";
import type { DepartmentId } from "@shared/types";
-import { formatSkillsForPrompt } from "@mariozechner/pi-coding-agent";
import {
buildTaskSection,
buildReviewSection,
@@ -30,27 +29,39 @@ function isLeadershipRole(ctx: PromptContext): boolean {
}
/**
- * Render the skill manifest block using pi-coding-agent's XML format.
- *
- * The full SKILL.md content is NOT injected — only name/description/filePath.
- * The agent loads SKILL.md on demand via the `read` tool (progressive
- * disclosure). Pi's `formatSkillsForPrompt` expects its full `Skill` shape, so
- * we supply synthetic values for the two fields we don't track (`sourceInfo`,
- * `disableModelInvocation`). Cast is safe because pi's XML renderer only reads
- * `name`, `description`, `filePath`, and `disableModelInvocation`.
+ * Render the Agent Skills manifest from the runtime projection. The prompt
+ * carries only name, description, and SKILL.md location; the agent reads the
+ * full file on demand through the filesystem tool.
*/
function buildSkillManifest(skills: ProjectedSkill[] | undefined): string {
if (!skills || skills.length === 0) return "";
- const piShapedSkills = skills.map((s) => ({
- name: s.name,
- description: s.description,
- filePath: s.filePath,
- baseDir: s.baseDir,
- disableModelInvocation: false,
- // sourceInfo is required by pi's Skill interface but never read by
- // formatSkillsForPrompt. Leaving it undefined and asserting the shape.
- }));
- return formatSkillsForPrompt(piShapedSkills as never);
+ 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.",
+ "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.",
+ "",
+ "",
+ ];
+
+ for (const skill of skills) {
+ lines.push(" ");
+ lines.push(` ${escapeSkillXml(skill.name)}`);
+ lines.push(` ${escapeSkillXml(skill.description)}`);
+ lines.push(` ${escapeSkillXml(skill.filePath)}`);
+ lines.push(" ");
+ }
+
+ lines.push("");
+ return lines.join("\n");
+}
+
+function escapeSkillXml(value: string): string {
+ return value
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'");
}
export function buildSystemPrompt(ctx: PromptContext): string {
diff --git a/tests/unit/prompt-manager-classification.test.ts b/tests/unit/prompt-manager-classification.test.ts
index ff8affee..322679a6 100644
--- a/tests/unit/prompt-manager-classification.test.ts
+++ b/tests/unit/prompt-manager-classification.test.ts
@@ -79,4 +79,20 @@ describe("buildSystemPrompt leadership classification", () => {
expect(prompt).toContain("New hires for your team should report to you by default");
expect(prompt).not.toContain("Operating Instructions (Individual Contributor)");
});
+
+ it("renders projected skills without pi skill shape casts", () => {
+ const prompt = buildSystemPrompt(buildCtx({
+ skills: [{
+ name: "ui&qa",
+ description: "Use for A&B \"checks\" and 'snapshots'.",
+ filePath: "/tmp/skills/ui&qa/SKILL.md",
+ baseDir: "/tmp/skills/ui&qa",
+ }],
+ }));
+
+ expect(prompt).toContain("");
+ expect(prompt).toContain("ui<review>&qa");
+ expect(prompt).toContain("Use for A&B "checks" and 'snapshots'.");
+ expect(prompt).toContain("/tmp/skills/ui<review>&qa/SKILL.md");
+ });
});