diff --git a/README.md b/README.md index c8c670b82..0f58f992f 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,13 @@ pnpm clawdis agent --message "Ship checklist" --thinking high pnpm clawdis gateway --force ``` +### Agent workspace + skills + +Clawdis runs the embedded agent with its working directory set to the agent workspace (default: `~/clawd`, configurable via `inbound.workspace`). + +- Workspace files injected into the system prompt: `AGENTS.md`, `SOUL.md`, `TOOLS.md` +- Custom skills: `/skills//SKILL.md` (default: `~/clawd/skills//SKILL.md`; only this location is scanned) + ## Companion Apps ### macOS Companion (Clawdis.app) diff --git a/src/agents/pi-embedded.ts b/src/agents/pi-embedded.ts index 39878aee0..b5ae33a90 100644 --- a/src/agents/pi-embedded.ts +++ b/src/agents/pi-embedded.ts @@ -43,6 +43,7 @@ import { createClawdisCodingTools, sanitizeContentBlocksImages, } from "./pi-tools.js"; +import { buildWorkspaceSkillsPrompt } from "./skills.js"; import { buildAgentSystemPrompt } from "./system-prompt.js"; import { loadWorkspaceBootstrapFiles } from "./workspace.js"; @@ -257,13 +258,15 @@ export async function runEmbeddedPiAgent(params: { })), defaultThinkLevel: params.thinkLevel, }); + const systemPromptWithSkills = + systemPrompt + buildWorkspaceSkillsPrompt(resolvedWorkspace); const sessionManager = new SessionManager(false, params.sessionFile); const settingsManager = new SettingsManager(); const agent = new Agent({ initialState: { - systemPrompt, + systemPrompt: systemPromptWithSkills, model, thinkingLevel, // TODO(steipete): Once pi-mono publishes file-magic MIME detection in `read` image payloads, diff --git a/src/agents/skills.test.ts b/src/agents/skills.test.ts new file mode 100644 index 000000000..2b7c4d463 --- /dev/null +++ b/src/agents/skills.test.ts @@ -0,0 +1,32 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { buildWorkspaceSkillsPrompt } from "./skills.js"; + +describe("buildWorkspaceSkillsPrompt", () => { + it("loads skills from workspace skills/", async () => { + const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-")); + const skillDir = path.join(workspaceDir, "skills", "demo-skill"); + await fs.mkdir(skillDir, { recursive: true }); + + await fs.writeFile( + path.join(skillDir, "SKILL.md"), + `--- +name: demo-skill +description: Does demo things +--- + +# Demo Skill +`, + "utf-8", + ); + + const prompt = buildWorkspaceSkillsPrompt(workspaceDir); + expect(prompt).toContain("demo-skill"); + expect(prompt).toContain("Does demo things"); + expect(prompt).toContain(path.join(skillDir, "SKILL.md")); + }); +}); diff --git a/src/agents/skills.ts b/src/agents/skills.ts new file mode 100644 index 000000000..f321d9ec9 --- /dev/null +++ b/src/agents/skills.ts @@ -0,0 +1,15 @@ +import path from "node:path"; + +import { + formatSkillsForPrompt, + loadSkillsFromDir, +} from "@mariozechner/pi-coding-agent"; + +export function buildWorkspaceSkillsPrompt(workspaceDir: string): string { + const skillsDir = path.join(workspaceDir, "skills"); + const skills = loadSkillsFromDir({ + dir: skillsDir, + source: "clawdis-workspace", + }); + return formatSkillsForPrompt(skills); +}