Files
pbakaus_impeccable/scripts/lib/transformers/hooks.js
T
Claude 199810fb5e Fix Codex plugin hook load failure; bump skill to 3.9.1
Codex loads bundled plugin lifecycle hooks from `hooks/hooks.json` using a
strict schema that accepts only the top-level `hooks` field. The
plugin-packaged manifest carried a top-level `description`, so Codex rejected
the whole manifest with `unknown field description, expected hooks` and the
post-edit design detector never registered (issue #330).

Drop `description` from `buildClaudePluginHooksManifest()` and regenerate
`plugin/hooks/hooks.json`. The Claude Code plugin path is unaffected (it only
reads the `hooks` object). Add a regression assertion for the plugin artifact
and bump the skill version to 3.9.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GTTHY6uHwESUyAgjUEm7x
2026-07-01 17:56:41 +00:00

151 lines
4.8 KiB
JavaScript

/**
* Build-pipeline emitters for the Impeccable design hook.
*
* Two emission targets exist:
*
* 1. Project-local install (the `npx impeccable skills install` CLI path):
* - Claude Code: `.claude/settings.json` (${CLAUDE_PROJECT_DIR}-relative)
* - Codex: `.codex/hooks.json`
* - Cursor: `.cursor/hooks.json`
*
* 2. Claude Code plugin package (the marketplace / `/plugin install` path):
* - `plugin/hooks/hooks.json` (${CLAUDE_PLUGIN_ROOT}-relative)
*
* The plugin variant resolves the hook script relative to the installed plugin
* root rather than assuming a `.claude/skills/impeccable/` layout, so it stays
* correct wherever Claude Code unpacks the plugin.
*/
export const IMPECCABLE_HOOK_COMMAND_MARKER = 'skills/impeccable/scripts/hook.mjs';
const TIMEOUT_SECONDS = 5;
const STATUS_MESSAGE = 'Checking UI changes';
const CLAUDE_PROJECT_HOOK = '${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs';
const CLAUDE_PLUGIN_HOOK = '${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/hook.mjs';
const CODEX_PROJECT_HOOK = '.agents/skills/impeccable/scripts/hook.mjs';
const CURSOR_BEFORE_EDIT_SCRIPT = '.cursor/skills/impeccable/scripts/hook-before-edit.mjs';
const GITHUB_PROJECT_HOOK = '$(git rev-parse --show-toplevel)/.github/skills/impeccable/scripts/hook.mjs';
export function buildClaudeSettingsManifest() {
return {
description: 'Impeccable design detector: runs after Edit/Write/MultiEdit on UI files and surfaces findings as system reminders.',
hooks: {
PostToolUse: [
{
matcher: 'Edit|Write|MultiEdit',
hooks: [
{
type: 'command',
command: `node "${CLAUDE_PROJECT_HOOK}"`,
timeout: TIMEOUT_SECONDS,
statusMessage: STATUS_MESSAGE,
},
],
},
],
},
};
}
// Plugin-packaged variant of the Claude hook. Claude Code reads the `hooks`
// object from a plugin's `hooks/hooks.json`, and the command resolves relative
// to ${CLAUDE_PLUGIN_ROOT} so it does not depend on the skill being copied into
// `.claude/skills/`. No top-level `description`: Codex also loads bundled plugin
// hooks from `hooks/hooks.json` and its strict parser rejects any field other
// than `hooks`, failing the whole manifest (issue #330).
export function buildClaudePluginHooksManifest() {
return {
hooks: {
PostToolUse: [
{
matcher: 'Edit|Write|MultiEdit',
hooks: [
{
type: 'command',
command: `node "${CLAUDE_PLUGIN_HOOK}"`,
timeout: TIMEOUT_SECONDS,
statusMessage: STATUS_MESSAGE,
},
],
},
],
},
};
}
export function buildCodexHooksManifest() {
return {
hooks: {
PostToolUse: [
{
matcher: 'Edit|Write|apply_patch',
hooks: [
{
type: 'command',
command: `node "${CODEX_PROJECT_HOOK}"`,
timeout: TIMEOUT_SECONDS,
statusMessage: STATUS_MESSAGE,
},
],
},
],
},
};
}
export function buildCursorHooksManifest() {
return {
version: 1,
hooks: {
preToolUse: [
{
command: `node "${CURSOR_BEFORE_EDIT_SCRIPT}"`,
timeout: TIMEOUT_SECONDS,
},
],
},
};
}
// GitHub Copilot reads project hooks from `.github/hooks/*.json`. Its schema
// differs from Claude/Codex/Cursor: the event key is lowercase `postToolUse`,
// each entry is flat (no nested `hooks` array), the command lives under `bash`
// (with an optional `powershell` sibling), the timeout key is `timeoutSec`, and
// `matcher` is a full-match regex (`^(?:PATTERN)$`) tested against the tool name.
// Copilot's file-editing tool names vary by surface (verified against CLI
// 1.0.63): `copilot -p` runs use `edit` ({path, old_str, new_str}) and `create`
// ({path, file_text}); interactive sessions and the cloud agent use
// `apply_patch` (a raw OpenAI-format patch string). The matcher covers all
// three. The same manifest is honored by both the CLI and the cloud/app agent.
// https://docs.github.com/en/copilot/reference/hooks-reference
export function buildGitHubHooksManifest() {
return {
version: 1,
hooks: {
postToolUse: [
{
type: 'command',
matcher: 'edit|create|apply_patch',
bash: `node "${GITHUB_PROJECT_HOOK}"`,
timeoutSec: TIMEOUT_SECONDS,
},
],
},
};
}
export function hooksJsonFor(provider) {
switch (provider) {
case 'claude':
return buildClaudeSettingsManifest();
case 'codex':
return buildCodexHooksManifest();
case 'cursor':
return buildCursorHooksManifest();
case 'github':
return buildGitHubHooksManifest();
default:
return null;
}
}