From 2465205356bacb40efa1b3d7a54dbafb7e77e4a3 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 7 Apr 2026 15:24:22 +0900 Subject: [PATCH] fix: propagate project skills to background task sessions (#2687) prompt-builder.ts now includes project-level skills from .opencode/skills/ when building delegated session prompts. 5007 tests pass, tsc clean. Closes #2687 --- .../delegate-task/prompt-builder.test.ts | 125 ++++++++++++++++++ src/tools/delegate-task/prompt-builder.ts | 31 ++++- 2 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 src/tools/delegate-task/prompt-builder.test.ts diff --git a/src/tools/delegate-task/prompt-builder.test.ts b/src/tools/delegate-task/prompt-builder.test.ts new file mode 100644 index 000000000..9c31fdefc --- /dev/null +++ b/src/tools/delegate-task/prompt-builder.test.ts @@ -0,0 +1,125 @@ +declare const require: (name: string) => unknown +const { describe, test, expect } = require("bun:test") as { + describe: (name: string, fn: () => void) => void + test: (name: string, fn: () => void) => void + expect: (value: unknown) => { + toBe: (expected: unknown) => void + toContain: (expected: string) => void + toBeUndefined: () => void + toBeDefined: () => void + not: { + toContain: (expected: string) => void + toBeUndefined: () => void + } + } +} + +import { buildSystemContent } from "./prompt-builder" +import type { AvailableSkill, AvailableCategory } from "../../agents/dynamic-agent-prompt-builder" + +describe("prompt-builder", () => { + describe("buildSystemContent", () => { + describe("#given non-plan agent with availableSkills", () => { + test("#when availableSkills contains project-level skills #then system content includes available_skills section", () => { + // given + const availableSkills: AvailableSkill[] = [ + { name: "git-master", description: "Git workflow automation", location: "plugin" }, + { name: "my-project-skill", description: "Project-specific deployment", location: "project" }, + ] + const availableCategories: AvailableCategory[] = [ + { name: "quick", description: "Trivial tasks", model: "openai/gpt-5.4-mini" }, + ] + + // when + const result = buildSystemContent({ + agentName: "sisyphus-junior", + availableSkills, + availableCategories, + }) + + // then + expect(result).toBeDefined() + expect(result).toContain("my-project-skill") + expect(result).toContain("git-master") + }) + + test("#when agent is explore #then system content includes available_skills section", () => { + // given + const availableSkills: AvailableSkill[] = [ + { name: "code-review", description: "Review code quality", location: "project" }, + ] + + // when + const result = buildSystemContent({ + agentName: "explore", + availableSkills, + }) + + // then + expect(result).toBeDefined() + expect(result).toContain("code-review") + }) + + test("#when availableSkills is empty #then system content does not include available_skills section", () => { + // given + const availableSkills: AvailableSkill[] = [] + + // when + const result = buildSystemContent({ + agentName: "sisyphus-junior", + availableSkills, + categoryPromptAppend: "some category context", + }) + + // then + expect(result).toBeDefined() + expect(result).not.toContain("available_skills") + }) + }) + + describe("#given plan agent with availableSkills", () => { + test("#when availableSkills provided #then system content includes plan agent prepend with skills", () => { + // given + const availableSkills: AvailableSkill[] = [ + { name: "git-master", description: "Git workflow automation", location: "plugin" }, + ] + const availableCategories: AvailableCategory[] = [ + { name: "quick", description: "Trivial tasks", model: "openai/gpt-5.4-mini" }, + ] + + // when + const result = buildSystemContent({ + agentName: "plan", + availableSkills, + availableCategories, + }) + + // then + expect(result).toBeDefined() + expect(result).toContain("git-master") + expect(result).toContain("AVAILABLE SKILLS") + }) + }) + + describe("#given non-plan agent with agentsContext override", () => { + test("#when agentsContext is provided #then it takes precedence and skills section is appended", () => { + // given + const availableSkills: AvailableSkill[] = [ + { name: "deploy-skill", description: "Deployment automation", location: "project" }, + ] + + // when + const result = buildSystemContent({ + agentName: "sisyphus-junior", + agentsContext: "Custom agent context here", + availableSkills, + }) + + // then + expect(result).toBeDefined() + expect(result).toContain("Custom agent context here") + expect(result).toContain("deploy-skill") + }) + }) + }) +}) diff --git a/src/tools/delegate-task/prompt-builder.ts b/src/tools/delegate-task/prompt-builder.ts index 1672eea74..838fac93f 100644 --- a/src/tools/delegate-task/prompt-builder.ts +++ b/src/tools/delegate-task/prompt-builder.ts @@ -1,4 +1,5 @@ import type { BuildSystemContentInput } from "./types" +import type { AvailableSkill } from "../../agents/dynamic-agent-prompt-builder" import { buildPlanAgentSystemPrepend, isPlanAgent } from "./constants" import { buildSystemContentWithTokenLimit } from "./token-limiter" @@ -21,6 +22,22 @@ ${TDD_LINE}` return PLAN_AGENT_PROMPT_BASE } +function buildAvailableSkillsSection(skills: AvailableSkill[]): string { + if (skills.length === 0) { + return "" + } + + const rows = skills + .map((s) => `- \`${s.name}\`: ${s.description || s.name}`) + .join("\n") + + return ` +Skills provide specialized instructions. Load via load_skills parameter when delegating tasks. + +${rows} +` +} + function usesFreeOrLocalModel(model: { providerID: string; modelID: string; variant?: string } | undefined): boolean { if (!model) { return false @@ -51,10 +68,20 @@ export function buildSystemContent(input: BuildSystemContentInput): string | und availableSkills, } = input - const planAgentPrepend = isPlanAgent(agentName) + const isPlan = isPlanAgent(agentName) + const planAgentPrepend = isPlan ? buildPlanAgentSystemPrepend(availableCategories, availableSkills) : "" + const skillsSection = !isPlan + ? buildAvailableSkillsSection(availableSkills ?? []) + : "" + + const baseAgentsContext = agentsContext ?? planAgentPrepend + const effectiveAgentsContext = !isPlan && skillsSection + ? [baseAgentsContext, skillsSection].filter(Boolean).join("\n\n") + : baseAgentsContext + const effectiveMaxPromptTokens = maxPromptTokens ?? (usesFreeOrLocalModel(model) ? FREE_OR_LOCAL_PROMPT_TOKEN_LIMIT : undefined) @@ -63,7 +90,7 @@ export function buildSystemContent(input: BuildSystemContentInput): string | und skillContent, skillContents, categoryPromptAppend, - agentsContext: agentsContext ?? planAgentPrepend, + agentsContext: effectiveAgentsContext, planAgentPrepend, }, effectiveMaxPromptTokens