fix(hooks): respect live skill workshop config
Some checks are pending
CI / preflight (push) Waiting to run
CI / security-scm-fast (push) Waiting to run
CI / security-dependency-audit (push) Waiting to run
CI / security-fast (push) Blocked by required conditions
CI / build-artifacts (push) Blocked by required conditions
CI / (push) Blocked by required conditions
CI / -1 (push) Blocked by required conditions
CI / checks-fast-contracts-channels (push) Blocked by required conditions
CI / checks-fast-protocol (push) Blocked by required conditions
CI / -2 (push) Blocked by required conditions
CI / checks-node-extensions (push) Blocked by required conditions
CI / -3 (push) Blocked by required conditions
CI / checks-node-compat-node22 (push) Blocked by required conditions
CI / -4 (push) Blocked by required conditions
CI / -5 (push) Blocked by required conditions
CI / checks-node-core (push) Blocked by required conditions
CI / extension-fast (push) Blocked by required conditions
CI / check-lint (push) Blocked by required conditions
CI / check-policy-guards (push) Blocked by required conditions
CI / check-preflight-guards (push) Blocked by required conditions
CI / check-prod-types (push) Blocked by required conditions
CI / check-strict-smoke (push) Blocked by required conditions
CI / check-test-types (push) Blocked by required conditions
CI / check (push) Blocked by required conditions
CI / check-additional-boundaries (push) Blocked by required conditions
CI / check-additional-extension-bundled (push) Blocked by required conditions
CI / check-additional-extension-channels (push) Blocked by required conditions
CI / check-additional-extension-package-boundary (push) Blocked by required conditions
CI / check-additional-runtime-topology-architecture (push) Blocked by required conditions
CI / check-additional-runtime-topology-gateway (push) Blocked by required conditions
CI / check-additional (push) Blocked by required conditions
CI / build-smoke (push) Blocked by required conditions
CI / check-docs (push) Blocked by required conditions
CI / skills-python (push) Blocked by required conditions
CI / -6 (push) Blocked by required conditions
CI / -7 (push) Blocked by required conditions
CI / macos-swift (push) Blocked by required conditions
CI / -8 (push) Blocked by required conditions
Docs Sync Publish Repo / sync-publish-repo (push) Waiting to run
Install Smoke / preflight (push) Waiting to run
Install Smoke / install-smoke (push) Blocked by required conditions
Plugin NPM Release / preview_plugins_npm (push) Waiting to run
Plugin NPM Release / preview_plugin_pack (push) Blocked by required conditions
Plugin NPM Release / publish_plugins_npm (push) Blocked by required conditions
Workflow Sanity / no-tabs (push) Waiting to run
Workflow Sanity / actionlint (push) Waiting to run
Workflow Sanity / generated-doc-baselines (push) Waiting to run

This commit is contained in:
Vincent Koc 2026-04-22 12:58:59 -07:00
parent 9ea5484fa1
commit fbf554397f
No known key found for this signature in database
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;
}