fix(hooks,tools): replace /plan example with prometheus delegation and rename code-review example to review-work (#2633, #3285, #2873)
- context-info-builder referenced a non-existent /plan command; now directs users to the Prometheus agent for planning - skill tool description example referenced 'code-review' which does not exist; changed to 'review-work' (actual built-in skill) - skill tool execute path now surfaces the missing host permission gap so callers understand OpenCode plugin context limits 🤖 Generated with OhMyOpenCode assistance https://github.com/code-yeongyu/oh-my-opencode
This commit is contained in:
@@ -8,7 +8,7 @@ Skills and commands provide specialized knowledge and step-by-step guidance.
|
||||
Use this when a task matches an available skill's or command's description.
|
||||
|
||||
**How to use:**
|
||||
- Call with a skill name: name='code-review'
|
||||
- Call with a skill name: name='review-work'
|
||||
- Call with a command name (without leading slash): name='publish'
|
||||
- The tool will return detailed instructions with your context applied.
|
||||
`
|
||||
|
||||
@@ -104,7 +104,7 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition
|
||||
return cachedDescription ?? TOOL_DESCRIPTION_PREFIX
|
||||
},
|
||||
args: {
|
||||
name: tool.schema.string().describe("The skill or command name (e.g., 'code-review' or 'publish'). Use without leading slash for commands."),
|
||||
name: tool.schema.string().describe("The skill or command name (e.g., 'review-work' or 'publish'). Use without leading slash for commands."),
|
||||
user_message: tool.schema
|
||||
.string()
|
||||
.optional()
|
||||
@@ -119,6 +119,15 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition
|
||||
const matchedSkill = matchSkillByName(skills, requestedName)
|
||||
|
||||
if (matchedSkill) {
|
||||
await ctx?.ask({
|
||||
permission: "skill",
|
||||
patterns: [matchedSkill.name],
|
||||
always: [matchedSkill.name],
|
||||
metadata: {
|
||||
skill: matchedSkill.name,
|
||||
},
|
||||
})
|
||||
|
||||
if (matchedSkill.definition.agent && (!ctx?.agent || matchedSkill.definition.agent !== ctx.agent)) {
|
||||
throw new Error(`Skill "${matchedSkill.name}" is restricted to agent "${matchedSkill.definition.agent}"`)
|
||||
}
|
||||
|
||||
@@ -127,6 +127,32 @@ describe("skill tool - agent restriction", () => {
|
||||
expect(result).toContain("public-skill")
|
||||
})
|
||||
|
||||
it("requests host skill permission before loading the skill", async () => {
|
||||
// given
|
||||
const loadedSkills = [createMockSkill("review-work")]
|
||||
const askCalls: Array<Parameters<ToolContext["ask"]>[0]> = []
|
||||
const tool = createSkillTool({ skills: loadedSkills })
|
||||
const context: ToolContext = {
|
||||
...mockContext,
|
||||
ask: async (input) => {
|
||||
askCalls.push(input)
|
||||
},
|
||||
}
|
||||
|
||||
// when
|
||||
await tool.execute({ name: "review-work" }, context)
|
||||
|
||||
// then
|
||||
expect(askCalls).toEqual([
|
||||
{
|
||||
permission: "skill",
|
||||
patterns: ["review-work"],
|
||||
always: ["review-work"],
|
||||
metadata: { skill: "review-work" },
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("allows skill when agent matches restriction", async () => {
|
||||
// given
|
||||
const loadedSkills = [createMockSkill("restricted-skill", { agent: "sisyphus" })]
|
||||
@@ -147,7 +173,7 @@ describe("skill tool - agent restriction", () => {
|
||||
const context = { ...mockContext, agent: "oracle" }
|
||||
|
||||
// when / #then
|
||||
await expect(tool.execute({ name: "sisyphus-only-skill" }, context)).rejects.toThrow(
|
||||
return expect(tool.execute({ name: "sisyphus-only-skill" }, context)).rejects.toThrow(
|
||||
'Skill "sisyphus-only-skill" is restricted to agent "sisyphus"'
|
||||
)
|
||||
})
|
||||
@@ -159,7 +185,7 @@ describe("skill tool - agent restriction", () => {
|
||||
const contextWithoutAgent = { ...mockContext, agent: undefined as unknown as string }
|
||||
|
||||
// when / #then
|
||||
await expect(tool.execute({ name: "sisyphus-only-skill" }, contextWithoutAgent)).rejects.toThrow(
|
||||
return expect(tool.execute({ name: "sisyphus-only-skill" }, contextWithoutAgent)).rejects.toThrow(
|
||||
'Skill "sisyphus-only-skill" is restricted to agent "sisyphus"'
|
||||
)
|
||||
})
|
||||
@@ -567,6 +593,7 @@ describe("skill tool - dynamic description cache invalidation", () => {
|
||||
|
||||
// Get initial description - it will build from empty or disk skills
|
||||
const initialDescription = tool.description
|
||||
expect(initialDescription).toBeString()
|
||||
|
||||
// when: execute() is called, which clears cache AND gets fresh skills
|
||||
// Note: In real scenario, execute() would discover new skills from disk
|
||||
@@ -739,7 +766,7 @@ describe("skill tool - short name resolution", () => {
|
||||
const tool = createSkillTool({ skills: loadedSkills })
|
||||
|
||||
// when / then, should not resolve (ambiguous), should suggest both
|
||||
await expect(tool.execute({ name: "debugging" }, mockContext)).rejects.toThrow(
|
||||
return expect(tool.execute({ name: "debugging" }, mockContext)).rejects.toThrow(
|
||||
"not found"
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user