Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-duplicate-skill-catalog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---

Remove the duplicate skill catalog from the model-facing skill tool description.
16 changes: 0 additions & 16 deletions packages/opencode/src/tool/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ const layer = Layer.effect(
const config = yield* Config.Service
const plugin = yield* Plugin.Service
const agents = yield* Agent.Service
const skill = yield* Skill.Service // kilocode_change - keep the available skill summary in model-facing tool context
const truncate = yield* Truncate.Service
const flags = yield* RuntimeFlags.Service
const mcp = yield* MCP.Service
Expand Down Expand Up @@ -344,20 +343,6 @@ const layer = Layer.effect(
return ["Available agent types and the tools they have access to:", description].join("\n")
})

// kilocode_change start - retain the concise skill inventory added to the skill tool description
const describeSkill = Effect.fn("ToolRegistry.describeSkill")(function* (agent: Agent.Info) {
const list = yield* skill.available(agent)
if (list.length === 0) return "No skills are currently available."
return [
"Load a specialized skill that provides domain-specific instructions and workflows.",
"",
"When a task matches one of the available skills below, load its full instructions with this tool.",
"",
Skill.fmt(list, { verbose: false }),
].join("\n")
})
// kilocode_change end

const describeCodeMode = Effect.fn("ToolRegistry.describeCodeMode")(function* (input: {
agent: Agent.Info
permission?: PermissionV1.Ruleset
Expand Down Expand Up @@ -412,7 +397,6 @@ const layer = Layer.effect(
description: [
output.description,
tool.id === TaskTool.id ? yield* describeTask(input.agent) : undefined,
tool.id === SkillTool.id ? yield* describeSkill(input.agent) : undefined,
tool.id === "execute" ? codeModeDescription : undefined,
]
.filter(Boolean)
Expand Down
73 changes: 73 additions & 0 deletions packages/opencode/test/kilocode/skill-catalog.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// kilocode_change - new file
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { expect } from "bun:test"
import { Effect } from "effect"
import fs from "fs/promises"
import path from "path"
import { Permission } from "../../src/permission"
import { SystemPrompt } from "../../src/session/system"
import { SkillTool } from "../../src/tool/skill"
import { ToolRegistry } from "../../src/tool/registry"
import { TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"

const it = testEffect(
LayerNode.compile(LayerNode.group([ToolRegistry.node, SystemPrompt.node, CrossSpawnSpawner.node, Ripgrep.node])),
)

const agent = {
name: "build",
mode: "primary" as const,
permission: Permission.fromConfig({ "*": "allow" }),
options: {},
}

function count(text: string, value: string) {
return text.split(value).length - 1
}

it.instance("exposes the available skill catalog once in model context", () =>
Effect.gen(function* () {
const instance = yield* TestInstance
const dir = path.join(instance.directory, ".kilo", "skill", "catalog-skill")
yield* Effect.promise(() => fs.mkdir(dir, { recursive: true }))
yield* Effect.promise(() =>
Bun.write(
path.join(dir, "SKILL.md"),
[
"---",
"name: catalog-skill",
"description: Catalog skill for deduplication tests.",
"---",
"",
"# Catalog Skill",
"",
"Full skill instructions stay out of model-facing metadata.",
"",
].join("\n"),
),
)

const system = yield* SystemPrompt.Service
const registry = yield* ToolRegistry.Service
const prompt = yield* system.skills(agent)
if (!prompt) throw new Error("skill catalog was not added to the system prompt")
const tool = (yield* registry.tools({
providerID: ProviderV2.ID.opencode,
modelID: ModelV2.ID.make("gpt-5"),
agent,
})).find((item) => item.id === SkillTool.id)
if (!tool) throw new Error("skill tool was not returned")

const context = [prompt, tool.description].join("\n")
expect(count(prompt, "<available_skills>")).toBe(1)
expect(count(context, "<available_skills>")).toBe(1)
expect(context).toContain("<name>catalog-skill</name>")
expect(context).not.toContain("# Catalog Skill")
expect(tool.description).toContain("skills listed in the system prompt")
}),
)
15 changes: 13 additions & 2 deletions packages/opencode/test/tool/skill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { Permission } from "../../src/permission"
import type { Tool } from "@/tool/tool"
import { SkillTool } from "../../src/tool/skill"
import { ToolRegistry } from "@/tool/registry"
import { ToolJsonSchema } from "@/tool/json-schema"
import { disposeAllInstances, provideTmpdirInstance, TestInstance } from "../fixture/fixture" // kilocode_change
import { SessionID, MessageID } from "../../src/session/schema"
import { testEffect } from "../lib/effect"
Expand Down Expand Up @@ -70,8 +71,17 @@ Use this skill.
})).find((tool) => tool.id === SkillTool.id)
if (!tool) throw new Error("Skill tool not found")

expect(tool.description).toContain("tool-skill") // kilocode_change - include concise available-skill context
expect(tool.description).toContain("Skill for tool tests.") // kilocode_change
expect(tool.description).not.toContain("tool-skill")
expect(tool.description).not.toContain("Skill for tool tests.")
expect(tool.description).not.toContain("# Tool Skill")
expect(tool.description).toContain("skills listed in the system prompt")
expect(ToolJsonSchema.fromTool(tool)).toMatchObject({
type: "object",
properties: {
name: { type: "string", description: "The name of the skill from available_skills" },
},
required: ["name"],
})

const requests: Array<Omit<PermissionV1.Request, "id" | "sessionID" | "tool">> = []
const ctx: Tool.Context = {
Expand All @@ -91,6 +101,7 @@ Use this skill.
expect(requests[0].always).toContain("tool-skill")
expect(result.metadata.dir).toBe(skill)
expect(result.output).toContain(`<skill_content name="tool-skill">`)
expect(result.output).toContain("Use this skill.")
expect(result.output).toContain(`Base directory for this skill: ${skill}`)
expect(result.output).toContain(`<file>${file}</file>`)
}),
Expand Down
Loading