feat(hooks): add category-skill-reminder hook (#1123)
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com> Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
90054b28ad
commit
da416b362b
@ -77,6 +77,7 @@ export const HookNameSchema = z.enum([
|
|||||||
|
|
||||||
"thinking-block-validator",
|
"thinking-block-validator",
|
||||||
"ralph-loop",
|
"ralph-loop",
|
||||||
|
"category-skill-reminder",
|
||||||
|
|
||||||
"compaction-context-injector",
|
"compaction-context-injector",
|
||||||
"claude-code-hooks",
|
"claude-code-hooks",
|
||||||
|
|||||||
346
src/hooks/category-skill-reminder/index.test.ts
Normal file
346
src/hooks/category-skill-reminder/index.test.ts
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test"
|
||||||
|
import { createCategorySkillReminderHook } from "./index"
|
||||||
|
import { updateSessionAgent, clearSessionAgent, _resetForTesting } from "../../features/claude-code-session-state"
|
||||||
|
import * as sharedModule from "../../shared"
|
||||||
|
|
||||||
|
describe("category-skill-reminder hook", () => {
|
||||||
|
let logCalls: Array<{ msg: string; data?: unknown }>
|
||||||
|
let logSpy: ReturnType<typeof spyOn>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
_resetForTesting()
|
||||||
|
logCalls = []
|
||||||
|
logSpy = spyOn(sharedModule, "log").mockImplementation((msg: string, data?: unknown) => {
|
||||||
|
logCalls.push({ msg, data })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
logSpy?.mockRestore()
|
||||||
|
})
|
||||||
|
|
||||||
|
function createMockPluginInput() {
|
||||||
|
return {
|
||||||
|
client: {
|
||||||
|
tui: {
|
||||||
|
showToast: async () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("target agent detection", () => {
|
||||||
|
test("should inject reminder for sisyphus agent after 3 tool calls", async () => {
|
||||||
|
// #given - sisyphus agent session with multiple tool calls
|
||||||
|
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
||||||
|
const sessionID = "sisyphus-session"
|
||||||
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
|
||||||
|
const output = { title: "", output: "file content", metadata: {} }
|
||||||
|
|
||||||
|
// #when - 3 edit tool calls are made
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "1" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "2" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "3" }, output)
|
||||||
|
|
||||||
|
// #then - reminder should be injected
|
||||||
|
expect(output.output).toContain("[Category+Skill Reminder]")
|
||||||
|
expect(output.output).toContain("delegate_task")
|
||||||
|
|
||||||
|
clearSessionAgent(sessionID)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should inject reminder for atlas agent", async () => {
|
||||||
|
// #given - atlas agent session
|
||||||
|
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
||||||
|
const sessionID = "atlas-session"
|
||||||
|
updateSessionAgent(sessionID, "Atlas")
|
||||||
|
|
||||||
|
const output = { title: "", output: "result", metadata: {} }
|
||||||
|
|
||||||
|
// #when - 3 tool calls are made
|
||||||
|
await hook["tool.execute.after"]({ tool: "bash", sessionID, callID: "1" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "bash", sessionID, callID: "2" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "bash", sessionID, callID: "3" }, output)
|
||||||
|
|
||||||
|
// #then - reminder should be injected
|
||||||
|
expect(output.output).toContain("[Category+Skill Reminder]")
|
||||||
|
|
||||||
|
clearSessionAgent(sessionID)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should inject reminder for sisyphus-junior agent", async () => {
|
||||||
|
// #given - sisyphus-junior agent session
|
||||||
|
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
||||||
|
const sessionID = "junior-session"
|
||||||
|
updateSessionAgent(sessionID, "sisyphus-junior")
|
||||||
|
|
||||||
|
const output = { title: "", output: "result", metadata: {} }
|
||||||
|
|
||||||
|
// #when - 3 tool calls are made
|
||||||
|
await hook["tool.execute.after"]({ tool: "write", sessionID, callID: "1" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "write", sessionID, callID: "2" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "write", sessionID, callID: "3" }, output)
|
||||||
|
|
||||||
|
// #then - reminder should be injected
|
||||||
|
expect(output.output).toContain("[Category+Skill Reminder]")
|
||||||
|
|
||||||
|
clearSessionAgent(sessionID)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should NOT inject reminder for non-target agents", async () => {
|
||||||
|
// #given - librarian agent session (not a target)
|
||||||
|
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
||||||
|
const sessionID = "librarian-session"
|
||||||
|
updateSessionAgent(sessionID, "librarian")
|
||||||
|
|
||||||
|
const output = { title: "", output: "result", metadata: {} }
|
||||||
|
|
||||||
|
// #when - 3 tool calls are made
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "1" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "2" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "3" }, output)
|
||||||
|
|
||||||
|
// #then - reminder should NOT be injected
|
||||||
|
expect(output.output).not.toContain("[Category+Skill Reminder]")
|
||||||
|
|
||||||
|
clearSessionAgent(sessionID)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should detect agent from input.agent when session state is empty", async () => {
|
||||||
|
// #given - no session state, agent provided in input
|
||||||
|
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
||||||
|
const sessionID = "input-agent-session"
|
||||||
|
|
||||||
|
const output = { title: "", output: "result", metadata: {} }
|
||||||
|
|
||||||
|
// #when - 3 tool calls with agent in input
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "1", agent: "Sisyphus" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "2", agent: "Sisyphus" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "3", agent: "Sisyphus" }, output)
|
||||||
|
|
||||||
|
// #then - reminder should be injected
|
||||||
|
expect(output.output).toContain("[Category+Skill Reminder]")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("delegation tool tracking", () => {
|
||||||
|
test("should NOT inject reminder if delegate_task is used", async () => {
|
||||||
|
// #given - sisyphus agent that uses delegate_task
|
||||||
|
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
||||||
|
const sessionID = "delegation-session"
|
||||||
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
|
||||||
|
const output = { title: "", output: "result", metadata: {} }
|
||||||
|
|
||||||
|
// #when - delegate_task is used, then more tool calls
|
||||||
|
await hook["tool.execute.after"]({ tool: "delegate_task", sessionID, callID: "1" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "2" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "3" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "4" }, output)
|
||||||
|
|
||||||
|
// #then - reminder should NOT be injected (delegation was used)
|
||||||
|
expect(output.output).not.toContain("[Category+Skill Reminder]")
|
||||||
|
|
||||||
|
clearSessionAgent(sessionID)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should NOT inject reminder if call_omo_agent is used", async () => {
|
||||||
|
// #given - sisyphus agent that uses call_omo_agent
|
||||||
|
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
||||||
|
const sessionID = "omo-agent-session"
|
||||||
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
|
||||||
|
const output = { title: "", output: "result", metadata: {} }
|
||||||
|
|
||||||
|
// #when - call_omo_agent is used first
|
||||||
|
await hook["tool.execute.after"]({ tool: "call_omo_agent", sessionID, callID: "1" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "2" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "3" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "4" }, output)
|
||||||
|
|
||||||
|
// #then - reminder should NOT be injected
|
||||||
|
expect(output.output).not.toContain("[Category+Skill Reminder]")
|
||||||
|
|
||||||
|
clearSessionAgent(sessionID)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should NOT inject reminder if task tool is used", async () => {
|
||||||
|
// #given - sisyphus agent that uses task tool
|
||||||
|
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
||||||
|
const sessionID = "task-session"
|
||||||
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
|
||||||
|
const output = { title: "", output: "result", metadata: {} }
|
||||||
|
|
||||||
|
// #when - task tool is used
|
||||||
|
await hook["tool.execute.after"]({ tool: "task", sessionID, callID: "1" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "2" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "3" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "4" }, output)
|
||||||
|
|
||||||
|
// #then - reminder should NOT be injected
|
||||||
|
expect(output.output).not.toContain("[Category+Skill Reminder]")
|
||||||
|
|
||||||
|
clearSessionAgent(sessionID)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("tool call counting", () => {
|
||||||
|
test("should NOT inject reminder before 3 tool calls", async () => {
|
||||||
|
// #given - sisyphus agent with only 2 tool calls
|
||||||
|
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
||||||
|
const sessionID = "few-calls-session"
|
||||||
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
|
||||||
|
const output = { title: "", output: "result", metadata: {} }
|
||||||
|
|
||||||
|
// #when - only 2 tool calls are made
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "1" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "2" }, output)
|
||||||
|
|
||||||
|
// #then - reminder should NOT be injected yet
|
||||||
|
expect(output.output).not.toContain("[Category+Skill Reminder]")
|
||||||
|
|
||||||
|
clearSessionAgent(sessionID)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should only inject reminder once per session", async () => {
|
||||||
|
// #given - sisyphus agent session
|
||||||
|
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
||||||
|
const sessionID = "once-session"
|
||||||
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
|
||||||
|
const output1 = { title: "", output: "result1", metadata: {} }
|
||||||
|
const output2 = { title: "", output: "result2", metadata: {} }
|
||||||
|
|
||||||
|
// #when - 6 tool calls are made (should trigger at 3, not again at 6)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "1" }, output1)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "2" }, output1)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "3" }, output1)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "4" }, output2)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "5" }, output2)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "6" }, output2)
|
||||||
|
|
||||||
|
// #then - reminder should be in output1 but not output2
|
||||||
|
expect(output1.output).toContain("[Category+Skill Reminder]")
|
||||||
|
expect(output2.output).not.toContain("[Category+Skill Reminder]")
|
||||||
|
|
||||||
|
clearSessionAgent(sessionID)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should only count delegatable work tools", async () => {
|
||||||
|
// #given - sisyphus agent with mixed tool calls
|
||||||
|
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
||||||
|
const sessionID = "mixed-tools-session"
|
||||||
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
|
||||||
|
const output = { title: "", output: "result", metadata: {} }
|
||||||
|
|
||||||
|
// #when - non-delegatable tools are called (should not count)
|
||||||
|
await hook["tool.execute.after"]({ tool: "lsp_goto_definition", sessionID, callID: "1" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "lsp_find_references", sessionID, callID: "2" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "lsp_symbols", sessionID, callID: "3" }, output)
|
||||||
|
|
||||||
|
// #then - reminder should NOT be injected (LSP tools don't count)
|
||||||
|
expect(output.output).not.toContain("[Category+Skill Reminder]")
|
||||||
|
|
||||||
|
clearSessionAgent(sessionID)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("event handling", () => {
|
||||||
|
test("should reset state on session.deleted event", async () => {
|
||||||
|
// #given - sisyphus agent with reminder already shown
|
||||||
|
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
||||||
|
const sessionID = "delete-session"
|
||||||
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
|
||||||
|
const output1 = { title: "", output: "result1", metadata: {} }
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "1" }, output1)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "2" }, output1)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "3" }, output1)
|
||||||
|
expect(output1.output).toContain("[Category+Skill Reminder]")
|
||||||
|
|
||||||
|
// #when - session is deleted and new session starts
|
||||||
|
await hook.event({ event: { type: "session.deleted", properties: { info: { id: sessionID } } } })
|
||||||
|
|
||||||
|
const output2 = { title: "", output: "result2", metadata: {} }
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "4" }, output2)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "5" }, output2)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "6" }, output2)
|
||||||
|
|
||||||
|
// #then - reminder should be shown again (state was reset)
|
||||||
|
expect(output2.output).toContain("[Category+Skill Reminder]")
|
||||||
|
|
||||||
|
clearSessionAgent(sessionID)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should reset state on session.compacted event", async () => {
|
||||||
|
// #given - sisyphus agent with reminder already shown
|
||||||
|
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
||||||
|
const sessionID = "compact-session"
|
||||||
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
|
||||||
|
const output1 = { title: "", output: "result1", metadata: {} }
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "1" }, output1)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "2" }, output1)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "3" }, output1)
|
||||||
|
expect(output1.output).toContain("[Category+Skill Reminder]")
|
||||||
|
|
||||||
|
// #when - session is compacted
|
||||||
|
await hook.event({ event: { type: "session.compacted", properties: { sessionID } } })
|
||||||
|
|
||||||
|
const output2 = { title: "", output: "result2", metadata: {} }
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "4" }, output2)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "5" }, output2)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "6" }, output2)
|
||||||
|
|
||||||
|
// #then - reminder should be shown again (state was reset)
|
||||||
|
expect(output2.output).toContain("[Category+Skill Reminder]")
|
||||||
|
|
||||||
|
clearSessionAgent(sessionID)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("case insensitivity", () => {
|
||||||
|
test("should handle tool names case-insensitively", async () => {
|
||||||
|
// #given - sisyphus agent with mixed case tool names
|
||||||
|
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
||||||
|
const sessionID = "case-session"
|
||||||
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
|
||||||
|
const output = { title: "", output: "result", metadata: {} }
|
||||||
|
|
||||||
|
// #when - tool calls with different cases
|
||||||
|
await hook["tool.execute.after"]({ tool: "EDIT", sessionID, callID: "1" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "Edit", sessionID, callID: "2" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "3" }, output)
|
||||||
|
|
||||||
|
// #then - reminder should be injected (all counted)
|
||||||
|
expect(output.output).toContain("[Category+Skill Reminder]")
|
||||||
|
|
||||||
|
clearSessionAgent(sessionID)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should handle delegation tool names case-insensitively", async () => {
|
||||||
|
// #given - sisyphus agent using DELEGATE_TASK in uppercase
|
||||||
|
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
||||||
|
const sessionID = "case-delegate-session"
|
||||||
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
|
||||||
|
const output = { title: "", output: "result", metadata: {} }
|
||||||
|
|
||||||
|
// #when - DELEGATE_TASK in uppercase is used
|
||||||
|
await hook["tool.execute.after"]({ tool: "DELEGATE_TASK", sessionID, callID: "1" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "2" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "3" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "4" }, output)
|
||||||
|
|
||||||
|
// #then - reminder should NOT be injected (delegation was detected)
|
||||||
|
expect(output.output).not.toContain("[Category+Skill Reminder]")
|
||||||
|
|
||||||
|
clearSessionAgent(sessionID)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
165
src/hooks/category-skill-reminder/index.ts
Normal file
165
src/hooks/category-skill-reminder/index.ts
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
|
import { getSessionAgent } from "../../features/claude-code-session-state"
|
||||||
|
import { log } from "../../shared"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Target agents that should receive category+skill reminders.
|
||||||
|
* These are orchestrator agents that delegate work to specialized agents.
|
||||||
|
*/
|
||||||
|
const TARGET_AGENTS = new Set([
|
||||||
|
"sisyphus",
|
||||||
|
"sisyphus-junior",
|
||||||
|
"atlas",
|
||||||
|
])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tools that indicate the agent is doing work that could potentially be delegated.
|
||||||
|
* When these tools are used, we remind the agent about the category+skill system.
|
||||||
|
*/
|
||||||
|
const DELEGATABLE_WORK_TOOLS = new Set([
|
||||||
|
"edit",
|
||||||
|
"write",
|
||||||
|
"bash",
|
||||||
|
"read",
|
||||||
|
"grep",
|
||||||
|
"glob",
|
||||||
|
])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tools that indicate the agent is already using delegation properly.
|
||||||
|
*/
|
||||||
|
const DELEGATION_TOOLS = new Set([
|
||||||
|
"delegate_task",
|
||||||
|
"call_omo_agent",
|
||||||
|
"task",
|
||||||
|
])
|
||||||
|
|
||||||
|
const REMINDER_MESSAGE = `
|
||||||
|
[Category+Skill Reminder]
|
||||||
|
|
||||||
|
You are an orchestrator agent. Consider whether this work should be delegated:
|
||||||
|
|
||||||
|
**DELEGATE when:**
|
||||||
|
- UI/Frontend work → category: "visual-engineering", skills: ["frontend-ui-ux"]
|
||||||
|
- Complex logic/architecture → category: "ultrabrain"
|
||||||
|
- Quick/trivial tasks → category: "quick"
|
||||||
|
- Git operations → skills: ["git-master"]
|
||||||
|
- Browser automation → skills: ["playwright"] or ["agent-browser"]
|
||||||
|
|
||||||
|
**DO IT YOURSELF when:**
|
||||||
|
- Gathering context/exploring codebase
|
||||||
|
- Simple edits that are part of a larger task you're coordinating
|
||||||
|
- Tasks requiring your full context understanding
|
||||||
|
|
||||||
|
Example delegation:
|
||||||
|
\`\`\`
|
||||||
|
delegate_task(
|
||||||
|
category="visual-engineering",
|
||||||
|
load_skills=["frontend-ui-ux"],
|
||||||
|
description="Implement responsive navbar with animations",
|
||||||
|
run_in_background=true
|
||||||
|
)
|
||||||
|
\`\`\`
|
||||||
|
`
|
||||||
|
|
||||||
|
interface ToolExecuteInput {
|
||||||
|
tool: string
|
||||||
|
sessionID: string
|
||||||
|
callID: string
|
||||||
|
agent?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolExecuteOutput {
|
||||||
|
title: string
|
||||||
|
output: string
|
||||||
|
metadata: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SessionState {
|
||||||
|
delegationUsed: boolean
|
||||||
|
reminderShown: boolean
|
||||||
|
toolCallCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCategorySkillReminderHook(_ctx: PluginInput) {
|
||||||
|
const sessionStates = new Map<string, SessionState>()
|
||||||
|
|
||||||
|
function getOrCreateState(sessionID: string): SessionState {
|
||||||
|
if (!sessionStates.has(sessionID)) {
|
||||||
|
sessionStates.set(sessionID, {
|
||||||
|
delegationUsed: false,
|
||||||
|
reminderShown: false,
|
||||||
|
toolCallCount: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return sessionStates.get(sessionID)!
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTargetAgent(sessionID: string, inputAgent?: string): boolean {
|
||||||
|
const agent = getSessionAgent(sessionID) ?? inputAgent
|
||||||
|
if (!agent) return false
|
||||||
|
const agentLower = agent.toLowerCase()
|
||||||
|
return TARGET_AGENTS.has(agentLower) ||
|
||||||
|
agentLower.includes("sisyphus") ||
|
||||||
|
agentLower.includes("atlas")
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolExecuteAfter = async (
|
||||||
|
input: ToolExecuteInput,
|
||||||
|
output: ToolExecuteOutput,
|
||||||
|
) => {
|
||||||
|
const { tool, sessionID } = input
|
||||||
|
const toolLower = tool.toLowerCase()
|
||||||
|
|
||||||
|
if (!isTargetAgent(sessionID, input.agent)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = getOrCreateState(sessionID)
|
||||||
|
|
||||||
|
if (DELEGATION_TOOLS.has(toolLower)) {
|
||||||
|
state.delegationUsed = true
|
||||||
|
log("[category-skill-reminder] Delegation tool used", { sessionID, tool })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DELEGATABLE_WORK_TOOLS.has(toolLower)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.toolCallCount++
|
||||||
|
|
||||||
|
if (state.toolCallCount >= 3 && !state.delegationUsed && !state.reminderShown) {
|
||||||
|
output.output += REMINDER_MESSAGE
|
||||||
|
state.reminderShown = true
|
||||||
|
log("[category-skill-reminder] Reminder injected", {
|
||||||
|
sessionID,
|
||||||
|
toolCallCount: state.toolCallCount
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventHandler = async ({ event }: { event: { type: string; properties?: unknown } }) => {
|
||||||
|
const props = event.properties as Record<string, unknown> | undefined
|
||||||
|
|
||||||
|
if (event.type === "session.deleted") {
|
||||||
|
const sessionInfo = props?.info as { id?: string } | undefined
|
||||||
|
if (sessionInfo?.id) {
|
||||||
|
sessionStates.delete(sessionInfo.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === "session.compacted") {
|
||||||
|
const sessionID = (props?.sessionID ??
|
||||||
|
(props?.info as { id?: string } | undefined)?.id) as string | undefined
|
||||||
|
if (sessionID) {
|
||||||
|
sessionStates.delete(sessionID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"tool.execute.after": toolExecuteAfter,
|
||||||
|
event: eventHandler,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,6 +22,7 @@ export { createNonInteractiveEnvHook } from "./non-interactive-env";
|
|||||||
export { createInteractiveBashSessionHook } from "./interactive-bash-session";
|
export { createInteractiveBashSessionHook } from "./interactive-bash-session";
|
||||||
|
|
||||||
export { createThinkingBlockValidatorHook } from "./thinking-block-validator";
|
export { createThinkingBlockValidatorHook } from "./thinking-block-validator";
|
||||||
|
export { createCategorySkillReminderHook } from "./category-skill-reminder";
|
||||||
export { createRalphLoopHook, type RalphLoopHook } from "./ralph-loop";
|
export { createRalphLoopHook, type RalphLoopHook } from "./ralph-loop";
|
||||||
export { createAutoSlashCommandHook } from "./auto-slash-command";
|
export { createAutoSlashCommandHook } from "./auto-slash-command";
|
||||||
export { createEditErrorRecoveryHook } from "./edit-error-recovery";
|
export { createEditErrorRecoveryHook } from "./edit-error-recovery";
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import {
|
|||||||
createInteractiveBashSessionHook,
|
createInteractiveBashSessionHook,
|
||||||
|
|
||||||
createThinkingBlockValidatorHook,
|
createThinkingBlockValidatorHook,
|
||||||
|
createCategorySkillReminderHook,
|
||||||
createRalphLoopHook,
|
createRalphLoopHook,
|
||||||
createAutoSlashCommandHook,
|
createAutoSlashCommandHook,
|
||||||
createEditErrorRecoveryHook,
|
createEditErrorRecoveryHook,
|
||||||
@ -189,6 +190,10 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
? createThinkingBlockValidatorHook()
|
? createThinkingBlockValidatorHook()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const categorySkillReminder = isHookEnabled("category-skill-reminder")
|
||||||
|
? createCategorySkillReminderHook(ctx)
|
||||||
|
: null;
|
||||||
|
|
||||||
const ralphLoop = isHookEnabled("ralph-loop")
|
const ralphLoop = isHookEnabled("ralph-loop")
|
||||||
? createRalphLoopHook(ctx, {
|
? createRalphLoopHook(ctx, {
|
||||||
config: pluginConfig.ralph_loop,
|
config: pluginConfig.ralph_loop,
|
||||||
@ -434,6 +439,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
await thinkMode?.event(input);
|
await thinkMode?.event(input);
|
||||||
await anthropicContextWindowLimitRecovery?.event(input);
|
await anthropicContextWindowLimitRecovery?.event(input);
|
||||||
await agentUsageReminder?.event(input);
|
await agentUsageReminder?.event(input);
|
||||||
|
await categorySkillReminder?.event(input);
|
||||||
await interactiveBashSession?.event(input);
|
await interactiveBashSession?.event(input);
|
||||||
await ralphLoop?.event(input);
|
await ralphLoop?.event(input);
|
||||||
await atlasHook?.handler(input);
|
await atlasHook?.handler(input);
|
||||||
@ -601,6 +607,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
await rulesInjector?.["tool.execute.after"](input, output);
|
await rulesInjector?.["tool.execute.after"](input, output);
|
||||||
await emptyTaskResponseDetector?.["tool.execute.after"](input, output);
|
await emptyTaskResponseDetector?.["tool.execute.after"](input, output);
|
||||||
await agentUsageReminder?.["tool.execute.after"](input, output);
|
await agentUsageReminder?.["tool.execute.after"](input, output);
|
||||||
|
await categorySkillReminder?.["tool.execute.after"](input, output);
|
||||||
await interactiveBashSession?.["tool.execute.after"](input, output);
|
await interactiveBashSession?.["tool.execute.after"](input, output);
|
||||||
await editErrorRecovery?.["tool.execute.after"](input, output);
|
await editErrorRecovery?.["tool.execute.after"](input, output);
|
||||||
await delegateTaskRetry?.["tool.execute.after"](input, output);
|
await delegateTaskRetry?.["tool.execute.after"](input, output);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user