fix(hooks): respect live skill workshop config

This commit is contained in:
Vincent Koc
2026-04-22 12:58:59 -07:00
parent 9ea5484fa1
commit fbf554397f
2 changed files with 224 additions and 9 deletions

View File

@@ -213,6 +213,117 @@ describe("skill-workshop", () => {
});
});
it("uses live runtime config for prompt-build guidance enablement", async () => {
let configFile: Record<string, unknown> = {
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<string, unknown> = {
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<string, unknown> = {
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({

View File

@@ -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<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: 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;
}