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:
@@ -6,10 +6,7 @@ import {
|
|||||||
findPrometheusPlans,
|
findPrometheusPlans,
|
||||||
getPlanName,
|
getPlanName,
|
||||||
getPlanProgress,
|
getPlanProgress,
|
||||||
getTaskSessionState,
|
|
||||||
readBoulderState,
|
readBoulderState,
|
||||||
readCurrentTopLevelTask,
|
|
||||||
upsertTaskSessionState,
|
|
||||||
writeBoulderState,
|
writeBoulderState,
|
||||||
} from "../../features/boulder-state"
|
} from "../../features/boulder-state"
|
||||||
import { log } from "../../shared/logger"
|
import { log } from "../../shared/logger"
|
||||||
@@ -97,8 +94,8 @@ Ask the user which plan to work on.`
|
|||||||
return `
|
return `
|
||||||
## Plan Not Found
|
## Plan Not Found
|
||||||
|
|
||||||
Could not find a plan matching "${explicitPlanName}".
|
Could not find a plan matching "${explicitPlanName}".
|
||||||
No incomplete plans available. Create a new plan with: /plan "your task"`
|
No incomplete plans available. Create a new plan using the Prometheus agent.`
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildExplicitPlanContext(params: {
|
function buildExplicitPlanContext(params: {
|
||||||
@@ -125,8 +122,8 @@ function buildExplicitPlanContext(params: {
|
|||||||
return `
|
return `
|
||||||
## Plan Already Complete
|
## Plan Already Complete
|
||||||
|
|
||||||
The requested plan "${getPlanName(matchedPlan)}" has been completed.
|
The requested plan "${getPlanName(matchedPlan)}" has been completed.
|
||||||
All ${progress.total} tasks are done. Create a new plan with: /plan "your task"`
|
All ${progress.total} tasks are done. Create a new plan using the Prometheus agent.`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingState) {
|
if (existingState) {
|
||||||
@@ -224,8 +221,8 @@ function buildPlanDiscoveryContext(params: {
|
|||||||
return contextInfo + `
|
return contextInfo + `
|
||||||
## No Plans Found
|
## No Plans Found
|
||||||
|
|
||||||
No Prometheus plan files found at .sisyphus/plans/
|
No Prometheus plan files found in the .sisyphus plans directory.
|
||||||
Use Prometheus to create a work plan first: /plan "your task"`
|
Use the Prometheus agent to create a work plan first.`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (incompletePlans.length === 0) {
|
if (incompletePlans.length === 0) {
|
||||||
@@ -233,7 +230,7 @@ Use Prometheus to create a work plan first: /plan "your task"`
|
|||||||
|
|
||||||
## All Plans Complete
|
## All Plans Complete
|
||||||
|
|
||||||
All ${plans.length} plan(s) are complete. Create a new plan with: /plan "your task"`
|
All ${plans.length} plan(s) are complete. Create a new plan using the Prometheus agent.`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (incompletePlans.length === 1) {
|
if (incompletePlans.length === 1) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { join } from "node:path"
|
|||||||
import { tmpdir } from "node:os"
|
import { tmpdir } from "node:os"
|
||||||
import { randomUUID } from "node:crypto"
|
import { randomUUID } from "node:crypto"
|
||||||
import { createStartWorkHook } from "./index"
|
import { createStartWorkHook } from "./index"
|
||||||
|
import { buildStartWorkContextInfo } from "./context-info-builder"
|
||||||
import { createAtlasHook } from "../atlas"
|
import { createAtlasHook } from "../atlas"
|
||||||
import {
|
import {
|
||||||
writeBoulderState,
|
writeBoulderState,
|
||||||
@@ -67,6 +68,27 @@ You are starting a Sisyphus work session.
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("chat.message handler", () => {
|
describe("chat.message handler", () => {
|
||||||
|
test("should not include /plan literal in missing-plan guidance", () => {
|
||||||
|
// given
|
||||||
|
const contextInfo = buildStartWorkContextInfo({
|
||||||
|
ctx: createMockPluginInput(),
|
||||||
|
explicitPlanName: null,
|
||||||
|
existingState: null,
|
||||||
|
sessionId: "session-123",
|
||||||
|
timestamp: "2026-04-12T00:00:00.000Z",
|
||||||
|
activeAgent: "sisyphus",
|
||||||
|
worktreePath: undefined,
|
||||||
|
worktreeBlock: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
// when
|
||||||
|
const containsLegacyPlanCommand = contextInfo.includes("/plan")
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(containsLegacyPlanCommand).toBe(false)
|
||||||
|
expect(contextInfo).toContain("Prometheus")
|
||||||
|
})
|
||||||
|
|
||||||
test("should ignore non-start-work commands", async () => {
|
test("should ignore non-start-work commands", async () => {
|
||||||
// given - hook and non-start-work message
|
// given - hook and non-start-work message
|
||||||
const hook = createStartWorkHook(createMockPluginInput())
|
const hook = createStartWorkHook(createMockPluginInput())
|
||||||
|
|||||||
@@ -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.
|
Use this when a task matches an available skill's or command's description.
|
||||||
|
|
||||||
**How to use:**
|
**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'
|
- Call with a command name (without leading slash): name='publish'
|
||||||
- The tool will return detailed instructions with your context applied.
|
- 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
|
return cachedDescription ?? TOOL_DESCRIPTION_PREFIX
|
||||||
},
|
},
|
||||||
args: {
|
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
|
user_message: tool.schema
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -119,6 +119,15 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition
|
|||||||
const matchedSkill = matchSkillByName(skills, requestedName)
|
const matchedSkill = matchSkillByName(skills, requestedName)
|
||||||
|
|
||||||
if (matchedSkill) {
|
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)) {
|
if (matchedSkill.definition.agent && (!ctx?.agent || matchedSkill.definition.agent !== ctx.agent)) {
|
||||||
throw new Error(`Skill "${matchedSkill.name}" is restricted to agent "${matchedSkill.definition.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")
|
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 () => {
|
it("allows skill when agent matches restriction", async () => {
|
||||||
// given
|
// given
|
||||||
const loadedSkills = [createMockSkill("restricted-skill", { agent: "sisyphus" })]
|
const loadedSkills = [createMockSkill("restricted-skill", { agent: "sisyphus" })]
|
||||||
@@ -147,7 +173,7 @@ describe("skill tool - agent restriction", () => {
|
|||||||
const context = { ...mockContext, agent: "oracle" }
|
const context = { ...mockContext, agent: "oracle" }
|
||||||
|
|
||||||
// when / #then
|
// 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"'
|
'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 }
|
const contextWithoutAgent = { ...mockContext, agent: undefined as unknown as string }
|
||||||
|
|
||||||
// when / #then
|
// 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"'
|
'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
|
// Get initial description - it will build from empty or disk skills
|
||||||
const initialDescription = tool.description
|
const initialDescription = tool.description
|
||||||
|
expect(initialDescription).toBeString()
|
||||||
|
|
||||||
// when: execute() is called, which clears cache AND gets fresh skills
|
// when: execute() is called, which clears cache AND gets fresh skills
|
||||||
// Note: In real scenario, execute() would discover new skills from disk
|
// 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 })
|
const tool = createSkillTool({ skills: loadedSkills })
|
||||||
|
|
||||||
// when / then, should not resolve (ambiguous), should suggest both
|
// 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"
|
"not found"
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user