From fbf554397fc4856ca3c323157f5c10222dc0fdaf Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 22 Apr 2026 12:58:59 -0700 Subject: [PATCH] fix(hooks): respect live skill workshop config --- extensions/skill-workshop/index.test.ts | 182 ++++++++++++++++++++++++ extensions/skill-workshop/index.ts | 51 +++++-- 2 files changed, 224 insertions(+), 9 deletions(-) diff --git a/extensions/skill-workshop/index.test.ts b/extensions/skill-workshop/index.test.ts index 750ca9f8dc6..d5e52ae0eca 100644 --- a/extensions/skill-workshop/index.test.ts +++ b/extensions/skill-workshop/index.test.ts @@ -213,6 +213,117 @@ describe("skill-workshop", () => { }); }); + it("uses live runtime config for prompt-build guidance enablement", async () => { + let configFile: Record = { + plugins: { + entries: { + "skill-workshop": { + config: { + approvalPolicy: "auto", + }, + }, + }, + }, + }; + const on = vi.fn(); + const api = createTestPluginApi({ + pluginConfig: { approvalPolicy: "auto" }, + runtime: { + config: { + loadConfig: () => configFile, + }, + } as never, + on, + }); + + plugin.register(api); + + const hook = on.mock.calls.find((call) => call[0] === "before_prompt_build")?.[1]; + expect(hook).toBeTypeOf("function"); + + configFile = { + plugins: { + entries: { + "skill-workshop": { + config: { + enabled: false, + }, + }, + }, + }, + }; + + await expect(hook?.({}, {})).resolves.toBeUndefined(); + }); + + it("uses live runtime config for tool approval policy", async () => { + const workspaceDir = await makeTempDir(); + const stateDir = await makeTempDir(); + let configFile: Record = { + plugins: { + entries: { + "skill-workshop": { + config: { + approvalPolicy: "pending", + }, + }, + }, + }, + }; + let tool: AnyAgentTool | undefined; + let toolFactory: + | ((ctx: { workspaceDir?: string }) => AnyAgentTool | AnyAgentTool[] | null | undefined) + | undefined; + const api = createTestPluginApi({ + pluginConfig: { approvalPolicy: "pending" }, + runtime: { + agent: { + resolveAgentWorkspaceDir: () => workspaceDir, + }, + state: { + resolveStateDir: () => stateDir, + }, + config: { + loadConfig: () => configFile, + }, + } as never, + registerTool(registered) { + toolFactory = typeof registered === "function" ? registered : undefined; + const resolved = + typeof registered === "function" ? registered({ workspaceDir }) : registered; + tool = Array.isArray(resolved) ? resolved[0] : (resolved ?? undefined); + }, + }); + + plugin.register(api); + + configFile = { + plugins: { + entries: { + "skill-workshop": { + config: { + approvalPolicy: "auto", + }, + }, + }, + }, + }; + const refreshedTool = toolFactory?.({ workspaceDir }); + tool = Array.isArray(refreshedTool) ? refreshedTool[0] : (refreshedTool ?? undefined); + + const result = await tool?.execute?.("call-1", { + action: "suggest", + skillName: "screenshot-asset-workflow", + description: "Screenshot asset workflow", + body: "Verify dimensions, optimize the PNG, and run the relevant gate.", + }); + + expect(result?.details).toMatchObject({ status: "applied" }); + await expect( + fs.access(path.join(workspaceDir, "skills", "screenshot-asset-workflow", "SKILL.md")), + ).resolves.toBeUndefined(); + }); + it("skips agent_end hook wiring when auto-capture is disabled", () => { const on = vi.fn(); const api = createTestPluginApi({ @@ -226,6 +337,77 @@ describe("skill-workshop", () => { expect(on).not.toHaveBeenCalledWith("agent_end", expect.any(Function)); }); + it("uses live runtime config to skip capture when review mode turns off", async () => { + const workspaceDir = await makeTempDir(); + const stateDir = await makeTempDir(); + let configFile: Record = { + plugins: { + entries: { + "skill-workshop": { + config: { + approvalPolicy: "auto", + reviewMode: "hybrid", + }, + }, + }, + }, + }; + const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }; + const on = vi.fn(); + const api = createTestPluginApi({ + pluginConfig: { approvalPolicy: "auto", reviewMode: "hybrid" }, + logger, + runtime: { + agent: { + resolveAgentWorkspaceDir: () => workspaceDir, + }, + state: { + resolveStateDir: () => stateDir, + }, + config: { + loadConfig: () => configFile, + }, + } as never, + on, + }); + + plugin.register(api); + + configFile = { + plugins: { + entries: { + "skill-workshop": { + config: { + approvalPolicy: "auto", + reviewMode: "off", + }, + }, + }, + }, + }; + + const handler = on.mock.calls.find((call) => call[0] === "agent_end")?.[1]; + expect(handler).toBeTypeOf("function"); + await handler?.( + { + success: true, + messages: [ + { + role: "user", + content: + "From now on when asked for animated GIFs, verify the file is actually animated.", + }, + ], + }, + { workspaceDir }, + ); + + await expect( + fs.access(path.join(workspaceDir, "skills", "animated-gif-workflow", "SKILL.md")), + ).rejects.toMatchObject({ code: "ENOENT" }); + expect(logger.info).not.toHaveBeenCalledWith("skill-workshop: applied animated-gif-workflow"); + }); + it("skips agent_end hook wiring when review mode is off", () => { const on = vi.fn(); const api = createTestPluginApi({ diff --git a/extensions/skill-workshop/index.ts b/extensions/skill-workshop/index.ts index 6ba61925427..43d201ddd27 100644 --- a/extensions/skill-workshop/index.ts +++ b/extensions/skill-workshop/index.ts @@ -6,27 +6,60 @@ import { createProposalFromMessages } from "./src/signals.js"; import { createSkillWorkshopTool } from "./src/tool.js"; import { applyOrStoreProposal, createStoreForContext } from "./src/workshop.js"; +function asRecord(value: unknown): Record | undefined { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : undefined; +} + export default definePluginEntry({ id: "skill-workshop", name: "Skill Workshop", description: "Captures repeatable workflows as workspace skills, with pending review and safe writes.", register(api) { - const config = resolveConfig(api.pluginConfig); - if (!config.enabled) { + const startupConfig = resolveConfig(api.pluginConfig); + if (!startupConfig.enabled) { return; } + const resolveCurrentConfig = () => { + const runtimeConfig = api.runtime.config?.loadConfig?.(); + const runtimePlugins = asRecord(asRecord(runtimeConfig)?.plugins); + const runtimeEntries = asRecord(runtimePlugins?.entries); + const runtimePluginConfig = + asRecord(runtimeEntries?.["skill-workshop"])?.config ?? api.pluginConfig; + return resolveConfig(runtimePluginConfig); + }; - api.registerTool((ctx) => createSkillWorkshopTool({ api, config, ctx }), { - name: "skill_workshop", + api.registerTool( + (ctx) => { + const config = resolveCurrentConfig(); + if (!config.enabled) { + return null; + } + return createSkillWorkshopTool({ api, config, ctx }); + }, + { + name: "skill_workshop", + }, + ); + + api.on("before_prompt_build", async () => { + const config = resolveCurrentConfig(); + if (!config.enabled) { + return undefined; + } + return { + prependSystemContext: buildWorkshopGuidance(config), + }; }); - api.on("before_prompt_build", async () => ({ - prependSystemContext: buildWorkshopGuidance(config), - })); - - if (config.autoCapture && config.reviewMode !== "off") { + if (startupConfig.autoCapture && startupConfig.reviewMode !== "off") { api.on("agent_end", async (event, ctx) => { + const config = resolveCurrentConfig(); + if (!config.enabled || !config.autoCapture || config.reviewMode === "off") { + return; + } if (!event.success) { return; }