From 5eb4080947d9d6660aa414f07a4af5deaa47a477 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 2 Feb 2026 00:08:28 -0600 Subject: [PATCH 01/12] tweak: adjust skill tool output to make it more clear which base directory the skill lives in --- packages/opencode/src/tool/skill.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts index 8f285d5999a0..6c49f208cd88 100644 --- a/packages/opencode/src/tool/skill.ts +++ b/packages/opencode/src/tool/skill.ts @@ -64,12 +64,14 @@ export const SkillTool = Tool.define("skill", async (ctx) => { const content = skill.content const dir = path.dirname(skill.location) - // Format output similar to plugin pattern - const output = [`## Skill: ${skill.name}`, "", `**Base directory**: ${dir}`, "", content.trim()].join("\n") - return { title: `Loaded skill: ${skill.name}`, - output, + output: [ + `Successfully loaded skill: ${skill.name}`, + `Base directory for this skill: ${dir}`, + "", + content.trim(), + ].join("\n"), metadata: { name: skill.name, dir, From 1403a8f8f7cfedac55cd473c751d29f6e3eabc4f Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 3 Feb 2026 00:05:22 -0600 Subject: [PATCH 02/12] tweaks --- packages/opencode/src/session/prompt.ts | 6 ++-- packages/opencode/src/tool/skill.ts | 47 +++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 98dce97ba90d..04faa14de8f3 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -407,10 +407,11 @@ export namespace SessionPrompt { } satisfies MessageV2.ToolPart) }, async ask(req) { + const latest = await Session.get(sessionID) await PermissionNext.ask({ ...req, sessionID: sessionID, - ruleset: PermissionNext.merge(taskAgent.permission, session.permission ?? []), + ruleset: PermissionNext.merge(taskAgent.permission, latest.permission ?? []), }) }, } @@ -699,11 +700,12 @@ export namespace SessionPrompt { } }, async ask(req) { + const latest = await Session.get(input.session.id) await PermissionNext.ask({ ...req, sessionID: input.session.id, tool: { messageID: input.processor.message.id, callID: options.toolCallId }, - ruleset: PermissionNext.merge(input.agent.permission, input.session.permission ?? []), + ruleset: PermissionNext.merge(input.agent.permission, latest.permission ?? []), }) }, }) diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts index 6c49f208cd88..ffe1b4f2b37d 100644 --- a/packages/opencode/src/tool/skill.ts +++ b/packages/opencode/src/tool/skill.ts @@ -3,6 +3,9 @@ import z from "zod" import { Tool } from "./tool" import { Skill } from "../skill" import { PermissionNext } from "../permission/next" +import { Session } from "../session" +import { Ripgrep } from "../file/ripgrep" +import { iife } from "@/util/iife" export const SkillTool = Tool.define("skill", async (ctx) => { const skills = await Skill.all() @@ -61,16 +64,54 @@ export const SkillTool = Tool.define("skill", async (ctx) => { always: [params.name], metadata: {}, }) - const content = skill.content + const dir = path.dirname(skill.location) + const limit = 10 + const files = await iife(async () => { + const arr = [] + for await (const file of Ripgrep.files({ + cwd: dir, + follow: false, + hidden: true, + signal: ctx.abort, + })) { + if (file.includes("SKILL.md")) { + continue + } + arr.push(path.resolve(dir, file)) + if (arr.length >= limit) { + break + } + } + return arr + }).then((f) => f.map((file) => `${file}`).join("\n")) + + await Session.update(ctx.sessionID, (draft) => { + const ruleset = draft.permission ?? [] + const glob = path.join(dir, "*") + if (!ruleset.some((r) => r.permission === "external_directory" && r.pattern === glob && r.action === "allow")) { + ruleset.push({ permission: "external_directory", pattern: glob, action: "allow" }) + } + draft.permission = ruleset + }) + return { title: `Loaded skill: ${skill.name}`, output: [ - `Successfully loaded skill: ${skill.name}`, + ``, + `# ${skill.name} Skill`, + "", + skill.content.trim(), + "", `Base directory for this skill: ${dir}`, + "Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.", + `Skill files list is limited to ${limit} entries and may be incomplete.`, "", - content.trim(), + "", + files, + "", + "", ].join("\n"), metadata: { name: skill.name, From 83d8dff552ccac3b5f4b669325bd52ca71f1dffe Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 3 Feb 2026 00:29:10 -0600 Subject: [PATCH 03/12] tweak --- packages/opencode/src/tool/skill.ts | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts index ffe1b4f2b37d..3e2f21e0dfe6 100644 --- a/packages/opencode/src/tool/skill.ts +++ b/packages/opencode/src/tool/skill.ts @@ -1,4 +1,5 @@ import path from "path" +import { pathToFileURL } from "url" import z from "zod" import { Tool } from "./tool" import { Skill } from "../skill" @@ -21,21 +22,29 @@ export const SkillTool = Tool.define("skill", async (ctx) => { const description = accessibleSkills.length === 0 - ? "Load a skill to get detailed instructions for a specific task. No skills are currently available." + ? "Load a specialized skill that provides domain-specific instructions and workflows. No skills are currently available." : [ - "Load a skill to get detailed instructions for a specific task.", - "Skills provide specialized knowledge and step-by-step guidance.", - "Use this when a task matches an available skill's description.", - "Only the skills listed here are available:", + "Load a specialized skill that provides domain-specific instructions and workflows.", + "", + "When you recognize that a task matches one of the available skills listed below, use this tool to load the full skill instructions.", + "", + "The skill will inject detailed instructions, workflows, and access to bundled resources (scripts, references, templates) into the conversation context.", + "", + 'Loaded skills appear as `` in the conversation.', + "", + "The following skills provide specialized sets of instructions for particular tasks", + "Invoke this tool to load a skill when a task matches one of the available skills listed below:", + "", "", ...accessibleSkills.flatMap((skill) => [ ` `, ` ${skill.name}`, ` ${skill.description}`, + ` ${pathToFileURL(skill.location).href}`, ` `, ]), "", - ].join(" ") + ].join("\n") const examples = accessibleSkills .map((skill) => `'${skill.name}'`) @@ -44,7 +53,7 @@ export const SkillTool = Tool.define("skill", async (ctx) => { const hint = examples.length > 0 ? ` (e.g., ${examples}, ...)` : "" const parameters = z.object({ - name: z.string().describe(`The skill identifier from available_skills${hint}`), + name: z.string().describe(`The name of the skill from available_skills${hint}`), }) return { @@ -66,6 +75,7 @@ export const SkillTool = Tool.define("skill", async (ctx) => { }) const dir = path.dirname(skill.location) + const base = pathToFileURL(dir).href const limit = 10 const files = await iife(async () => { @@ -104,7 +114,7 @@ export const SkillTool = Tool.define("skill", async (ctx) => { "", skill.content.trim(), "", - `Base directory for this skill: ${dir}`, + `Base directory for this skill: ${base}`, "Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.", `Skill files list is limited to ${limit} entries and may be incomplete.`, "", From 51e4cab96c4e78969a6d06a33a4de5e9291964a4 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 3 Feb 2026 00:35:11 -0600 Subject: [PATCH 04/12] tweaks --- packages/opencode/src/tool/skill.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts index 3e2f21e0dfe6..632275b6ba5f 100644 --- a/packages/opencode/src/tool/skill.ts +++ b/packages/opencode/src/tool/skill.ts @@ -30,7 +30,7 @@ export const SkillTool = Tool.define("skill", async (ctx) => { "", "The skill will inject detailed instructions, workflows, and access to bundled resources (scripts, references, templates) into the conversation context.", "", - 'Loaded skills appear as `` in the conversation.', + 'Tool output includes a `` block with the loaded content.', "", "The following skills provide specialized sets of instructions for particular tasks", "Invoke this tool to load a skill when a task matches one of the available skills listed below:", @@ -109,19 +109,19 @@ export const SkillTool = Tool.define("skill", async (ctx) => { return { title: `Loaded skill: ${skill.name}`, output: [ - ``, - `# ${skill.name} Skill`, + ``, + `# Skill: ${skill.name}`, "", skill.content.trim(), "", `Base directory for this skill: ${base}`, "Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.", - `Skill files list is limited to ${limit} entries and may be incomplete.`, + "Note: file list is sampled.", "", "", files, "", - "", + "", ].join("\n"), metadata: { name: skill.name, From 1e2d8fdb1868add79af68a3dd738b09a48bc3ac9 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 3 Feb 2026 00:54:15 -0600 Subject: [PATCH 05/12] wip --- packages/opencode/src/agent/agent.ts | 46 +++++++++++++++---------- packages/opencode/src/session/prompt.ts | 6 ++-- packages/opencode/src/skill/skill.ts | 15 ++++++-- packages/opencode/src/tool/skill.ts | 10 ------ 4 files changed, 42 insertions(+), 35 deletions(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 72e7f8985da1..9ad425f67b8b 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -18,6 +18,7 @@ import { mergeDeep, pipe, sortBy, values } from "remeda" import { Global } from "@/global" import path from "path" import { Plugin } from "@/plugin" +import { Skill } from "../skill" export namespace Agent { export const Info = z @@ -50,25 +51,34 @@ export namespace Agent { const state = Instance.state(async () => { const cfg = await Config.get() - const defaults = PermissionNext.fromConfig({ - "*": "allow", - doom_loop: "ask", - external_directory: { - "*": "ask", - [Truncate.DIR]: "allow", - [Truncate.GLOB]: "allow", - }, - question: "deny", - plan_enter: "deny", - plan_exit: "deny", - // mirrors github.com/github/gitignore Node.gitignore pattern for .env files - read: { + const dirs = await Skill.dirs() + const allow = dirs.map((dir) => ({ + permission: "external_directory", + pattern: path.join(dir, "*"), + action: "allow" as const, + })) + const defaults = PermissionNext.merge( + PermissionNext.fromConfig({ "*": "allow", - "*.env": "ask", - "*.env.*": "ask", - "*.env.example": "allow", - }, - }) + doom_loop: "ask", + external_directory: { + "*": "ask", + [Truncate.DIR]: "allow", + [Truncate.GLOB]: "allow", + }, + question: "deny", + plan_enter: "deny", + plan_exit: "deny", + // mirrors github.com/github/gitignore Node.gitignore pattern for .env files + read: { + "*": "allow", + "*.env": "ask", + "*.env.*": "ask", + "*.env.example": "allow", + }, + }), + allow, + ) const user = PermissionNext.fromConfig(cfg.permission ?? {}) const result: Record = { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 04faa14de8f3..98dce97ba90d 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -407,11 +407,10 @@ export namespace SessionPrompt { } satisfies MessageV2.ToolPart) }, async ask(req) { - const latest = await Session.get(sessionID) await PermissionNext.ask({ ...req, sessionID: sessionID, - ruleset: PermissionNext.merge(taskAgent.permission, latest.permission ?? []), + ruleset: PermissionNext.merge(taskAgent.permission, session.permission ?? []), }) }, } @@ -700,12 +699,11 @@ export namespace SessionPrompt { } }, async ask(req) { - const latest = await Session.get(input.session.id) await PermissionNext.ask({ ...req, sessionID: input.session.id, tool: { messageID: input.processor.message.id, callID: options.toolCallId }, - ruleset: PermissionNext.merge(input.agent.permission, latest.permission ?? []), + ruleset: PermissionNext.merge(input.agent.permission, input.session.permission ?? []), }) }, }) diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index 6e05d013ae5c..7ffc4d8a15fe 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -145,14 +145,23 @@ export namespace Skill { } } - return skills + const dirs = Array.from(new Set(Object.values(skills).map((item) => path.dirname(item.location)))) + + return { + skills, + dirs, + } }) export async function get(name: string) { - return state().then((x) => x[name]) + return state().then((x) => x.skills[name]) } export async function all() { - return state().then((x) => Object.values(x)) + return state().then((x) => Object.values(x.skills)) + } + + export async function dirs() { + return state().then((x) => x.dirs) } } diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts index 632275b6ba5f..8fcfb592dee6 100644 --- a/packages/opencode/src/tool/skill.ts +++ b/packages/opencode/src/tool/skill.ts @@ -4,7 +4,6 @@ import z from "zod" import { Tool } from "./tool" import { Skill } from "../skill" import { PermissionNext } from "../permission/next" -import { Session } from "../session" import { Ripgrep } from "../file/ripgrep" import { iife } from "@/util/iife" @@ -97,15 +96,6 @@ export const SkillTool = Tool.define("skill", async (ctx) => { return arr }).then((f) => f.map((file) => `${file}`).join("\n")) - await Session.update(ctx.sessionID, (draft) => { - const ruleset = draft.permission ?? [] - const glob = path.join(dir, "*") - if (!ruleset.some((r) => r.permission === "external_directory" && r.pattern === glob && r.action === "allow")) { - ruleset.push({ permission: "external_directory", pattern: glob, action: "allow" }) - } - draft.permission = ruleset - }) - return { title: `Loaded skill: ${skill.name}`, output: [ From b91e399e5f07a010a5953ce49882aef20376a94c Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 3 Feb 2026 00:54:23 -0600 Subject: [PATCH 06/12] wip --- .opencode/opencode.jsonc | 1 - 1 file changed, 1 deletion(-) diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index c3f0b7070d1a..8bc1b9d98983 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -1,6 +1,5 @@ { "$schema": "https://opencode.ai/config.json", - // "plugin": ["opencode-openai-codex-auth"], // "enterprise": { // "url": "https://enterprise.dev.opencode.ai", // }, From 5d2b631f5b0e1a0eb1e9556ddf799bf5e78f36b9 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 3 Feb 2026 00:58:55 -0600 Subject: [PATCH 07/12] fix --- packages/opencode/src/agent/agent.ts | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 35d51c7d2337..2cd1ebaf2f89 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -57,23 +57,12 @@ export namespace Agent { pattern: path.join(dir, "*"), action: "allow" as const, })) - const defaults = PermissionNext.fromConfig({ - "*": "allow", - doom_loop: "ask", - external_directory: { - "*": "ask", - [Truncate.GLOB]: "allow", - }, - question: "deny", - plan_enter: "deny", - plan_exit: "deny", - // mirrors github.com/github/gitignore Node.gitignore pattern for .env files - read: { + const defaults = PermissionNext.merge( + PermissionNext.fromConfig({ "*": "allow", doom_loop: "ask", external_directory: { "*": "ask", - [Truncate.DIR]: "allow", [Truncate.GLOB]: "allow", }, question: "deny", @@ -160,6 +149,7 @@ export namespace Agent { codesearch: "allow", read: "allow", external_directory: { + [Truncate.DIR]: "allow", [Truncate.GLOB]: "allow", }, }), @@ -248,19 +238,19 @@ export namespace Agent { item.permission = PermissionNext.merge(item.permission, PermissionNext.fromConfig(value.permission ?? {})) } - // Ensure Truncate.GLOB is allowed unless explicitly configured + // Ensure Truncate.DIR is allowed unless explicitly configured for (const name in result) { const agent = result[name] const explicit = agent.permission.some((r) => { if (r.permission !== "external_directory") return false if (r.action !== "deny") return false - return r.pattern === Truncate.GLOB + return r.pattern === Truncate.DIR || r.pattern === Truncate.GLOB }) if (explicit) continue result[name].permission = PermissionNext.merge( result[name].permission, - PermissionNext.fromConfig({ external_directory: { [Truncate.GLOB]: "allow" } }), + PermissionNext.fromConfig({ external_directory: { [Truncate.DIR]: "allow", [Truncate.GLOB]: "allow" } }), ) } From 81d6a6c7f7a7be0b880d072b33d551e182d52d01 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 3 Feb 2026 09:18:18 -0600 Subject: [PATCH 08/12] tweak: cleanup --- packages/opencode/src/agent/agent.ts | 43 ++++++++++++---------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 2cd1ebaf2f89..31bddbe83b82 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -52,32 +52,25 @@ export namespace Agent { const cfg = await Config.get() const dirs = await Skill.dirs() - const allow = dirs.map((dir) => ({ - permission: "external_directory", - pattern: path.join(dir, "*"), - action: "allow" as const, - })) - const defaults = PermissionNext.merge( - PermissionNext.fromConfig({ + const defaults = PermissionNext.fromConfig({ + "*": "allow", + doom_loop: "ask", + external_directory: { + "*": "ask", + [Truncate.GLOB]: "allow", + ...Object.fromEntries(dirs.map((dir) => [path.join(dir, "*"), "allow"])), + }, + question: "deny", + plan_enter: "deny", + plan_exit: "deny", + // mirrors github.com/github/gitignore Node.gitignore pattern for .env files + read: { "*": "allow", - doom_loop: "ask", - external_directory: { - "*": "ask", - [Truncate.GLOB]: "allow", - }, - question: "deny", - plan_enter: "deny", - plan_exit: "deny", - // mirrors github.com/github/gitignore Node.gitignore pattern for .env files - read: { - "*": "allow", - "*.env": "ask", - "*.env.*": "ask", - "*.env.example": "allow", - }, - }), - allow, - ) + "*.env": "ask", + "*.env.*": "ask", + "*.env.example": "allow", + }, + }) const user = PermissionNext.fromConfig(cfg.permission ?? {}) const result: Record = { From 54ebe02e4928880f9d1994873e5aac8407385f9e Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 3 Feb 2026 09:29:07 -0600 Subject: [PATCH 09/12] fix: tests --- packages/opencode/src/agent/agent.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 31bddbe83b82..d2137cf552d8 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -142,7 +142,6 @@ export namespace Agent { codesearch: "allow", read: "allow", external_directory: { - [Truncate.DIR]: "allow", [Truncate.GLOB]: "allow", }, }), @@ -231,19 +230,19 @@ export namespace Agent { item.permission = PermissionNext.merge(item.permission, PermissionNext.fromConfig(value.permission ?? {})) } - // Ensure Truncate.DIR is allowed unless explicitly configured + // Ensure Truncate.GLOB is allowed unless explicitly configured for (const name in result) { const agent = result[name] const explicit = agent.permission.some((r) => { if (r.permission !== "external_directory") return false if (r.action !== "deny") return false - return r.pattern === Truncate.DIR || r.pattern === Truncate.GLOB + return r.pattern === Truncate.GLOB }) if (explicit) continue result[name].permission = PermissionNext.merge( result[name].permission, - PermissionNext.fromConfig({ external_directory: { [Truncate.DIR]: "allow", [Truncate.GLOB]: "allow" } }), + PermissionNext.fromConfig({ external_directory: { [Truncate.GLOB]: "allow" } }), ) } From 59c5342a2283fa9bd392d7ffe8c819ad325ccd75 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 3 Feb 2026 09:30:23 -0600 Subject: [PATCH 10/12] tweak: var name --- packages/opencode/src/agent/agent.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index d2137cf552d8..e338559be7e4 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -51,14 +51,14 @@ export namespace Agent { const state = Instance.state(async () => { const cfg = await Config.get() - const dirs = await Skill.dirs() + const skillDirs = await Skill.dirs() const defaults = PermissionNext.fromConfig({ "*": "allow", doom_loop: "ask", external_directory: { "*": "ask", [Truncate.GLOB]: "allow", - ...Object.fromEntries(dirs.map((dir) => [path.join(dir, "*"), "allow"])), + ...Object.fromEntries(skillDirs.map((dir) => [path.join(dir, "*"), "allow"])), }, question: "deny", plan_enter: "deny", From 2a40fe6e92c96c5ec44434562dff80ceec99fb9a Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 3 Feb 2026 09:41:05 -0600 Subject: [PATCH 11/12] test: add tests --- packages/opencode/test/agent/agent.test.ts | 37 ++++++++++++++++++++++ packages/opencode/test/skill/skill.test.ts | 36 +++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index 05b8427394bc..5e91059ffb36 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -1,4 +1,5 @@ import { test, expect } from "bun:test" +import path from "path" import { tmpdir } from "../fixture/fixture" import { Instance } from "../../src/project/instance" import { Agent } from "../../src/agent/agent" @@ -513,6 +514,42 @@ test("explicit Truncate.GLOB deny is respected", async () => { }) }) +test("skill directories are allowed for external_directory", async () => { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + const skillDir = path.join(dir, ".opencode", "skill", "perm-skill") + await Bun.write( + path.join(skillDir, "SKILL.md"), + `--- +name: perm-skill +description: Permission skill. +--- + +# Permission Skill +`, + ) + }, + }) + + const home = process.env.OPENCODE_TEST_HOME + process.env.OPENCODE_TEST_HOME = tmp.path + + try { + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const build = await Agent.get("build") + const skillDir = path.join(tmp.path, ".opencode", "skill", "perm-skill") + const target = path.join(skillDir, "reference", "notes.md") + expect(PermissionNext.evaluate("external_directory", target, build!.permission).action).toBe("allow") + }, + }) + } finally { + process.env.OPENCODE_TEST_HOME = home + } +}) + test("defaultAgent returns build when no default_agent config", async () => { await using tmp = await tmpdir() await Instance.provide({ diff --git a/packages/opencode/test/skill/skill.test.ts b/packages/opencode/test/skill/skill.test.ts index 72415c1411e0..1d4828580ae8 100644 --- a/packages/opencode/test/skill/skill.test.ts +++ b/packages/opencode/test/skill/skill.test.ts @@ -55,6 +55,42 @@ Instructions here. }) }) +test("returns skill directories from Skill.dirs", async () => { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + const skillDir = path.join(dir, ".opencode", "skill", "dir-skill") + await Bun.write( + path.join(skillDir, "SKILL.md"), + `--- +name: dir-skill +description: Skill for dirs test. +--- + +# Dir Skill +`, + ) + }, + }) + + const home = process.env.OPENCODE_TEST_HOME + process.env.OPENCODE_TEST_HOME = tmp.path + + try { + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const dirs = await Skill.dirs() + const skillDir = path.join(tmp.path, ".opencode", "skill", "dir-skill") + expect(dirs).toContain(skillDir) + expect(dirs.length).toBe(1) + }, + }) + } finally { + process.env.OPENCODE_TEST_HOME = home + } +}) + test("discovers multiple skills from .opencode/skill/ directory", async () => { await using tmp = await tmpdir({ git: true, From a401adead50c9d404593e86f7ba0781e12ac08e4 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 3 Feb 2026 10:01:15 -0600 Subject: [PATCH 12/12] test: add test for new skills stuff --- packages/opencode/test/tool/skill.test.ts | 112 ++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 packages/opencode/test/tool/skill.test.ts diff --git a/packages/opencode/test/tool/skill.test.ts b/packages/opencode/test/tool/skill.test.ts new file mode 100644 index 000000000000..d5057ba9e7f4 --- /dev/null +++ b/packages/opencode/test/tool/skill.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, test } from "bun:test" +import path from "path" +import { pathToFileURL } from "url" +import type { PermissionNext } from "../../src/permission/next" +import type { Tool } from "../../src/tool/tool" +import { Instance } from "../../src/project/instance" +import { SkillTool } from "../../src/tool/skill" +import { tmpdir } from "../fixture/fixture" + +const baseCtx: Omit = { + sessionID: "test", + messageID: "", + callID: "", + agent: "build", + abort: AbortSignal.any([]), + messages: [], + metadata: () => {}, +} + +describe("tool.skill", () => { + test("description lists skill location URL", async () => { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + const skillDir = path.join(dir, ".opencode", "skill", "tool-skill") + await Bun.write( + path.join(skillDir, "SKILL.md"), + `--- +name: tool-skill +description: Skill for tool tests. +--- + +# Tool Skill +`, + ) + }, + }) + + const home = process.env.OPENCODE_TEST_HOME + process.env.OPENCODE_TEST_HOME = tmp.path + + try { + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const tool = await SkillTool.init() + const skillPath = path.join(tmp.path, ".opencode", "skill", "tool-skill", "SKILL.md") + expect(tool.description).toContain(`${pathToFileURL(skillPath).href}`) + }, + }) + } finally { + process.env.OPENCODE_TEST_HOME = home + } + }) + + test("execute returns skill content block with files", async () => { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + const skillDir = path.join(dir, ".opencode", "skill", "tool-skill") + await Bun.write( + path.join(skillDir, "SKILL.md"), + `--- +name: tool-skill +description: Skill for tool tests. +--- + +# Tool Skill + +Use this skill. +`, + ) + await Bun.write(path.join(skillDir, "scripts", "demo.txt"), "demo") + }, + }) + + const home = process.env.OPENCODE_TEST_HOME + process.env.OPENCODE_TEST_HOME = tmp.path + + try { + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const tool = await SkillTool.init() + const requests: Array> = [] + const ctx: Tool.Context = { + ...baseCtx, + ask: async (req) => { + requests.push(req) + }, + } + + const result = await tool.execute({ name: "tool-skill" }, ctx) + const dir = path.join(tmp.path, ".opencode", "skill", "tool-skill") + const file = path.resolve(dir, "scripts", "demo.txt") + + expect(requests.length).toBe(1) + expect(requests[0].permission).toBe("skill") + expect(requests[0].patterns).toContain("tool-skill") + expect(requests[0].always).toContain("tool-skill") + + expect(result.metadata.dir).toBe(dir) + expect(result.output).toContain(``) + expect(result.output).toContain(`Base directory for this skill: ${pathToFileURL(dir).href}`) + expect(result.output).toContain(`${file}`) + }, + }) + } finally { + process.env.OPENCODE_TEST_HOME = home + } + }) +})