diff --git a/.codex/hooks.json b/.codex/hooks.json index d7cd1b798..b6784765e 100644 --- a/.codex/hooks.json +++ b/.codex/hooks.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "[ ! -f \".codex/skills/impeccable/scripts/hook.mjs\" ] || ! { node -e \"import('fs')\" 2>/dev/null || { D=\"$HOME/.impeccable\"; [ -f \"$D/node-unsupported\" ] || { mkdir -p \"$D\" 2>/dev/null && : > \"$D/node-unsupported\" 2>/dev/null && printf '%s' '{\"systemMessage\":\"The impeccable design hook is not running: no Node 22 or newer on PATH. Install one, or remove the impeccable hook from your harness settings.\"}'; }; exit 0; }; } || node \".codex/skills/impeccable/scripts/hook.mjs\"", + "command": "[ ! -f \".codex/skills/impeccable/scripts/hook.mjs\" ] || ! node -e \"import('fs')\" 2>/dev/null || node \".codex/skills/impeccable/scripts/hook.mjs\"", "timeout": 5, "statusMessage": "Checking UI changes" } @@ -18,7 +18,7 @@ "hooks": [ { "type": "command", - "command": "[ ! -f \".codex/skills/impeccable/scripts/hook.mjs\" ] || ! { node -e \"import('fs')\" 2>/dev/null || { D=\"$HOME/.impeccable\"; [ -f \"$D/node-unsupported\" ] || { mkdir -p \"$D\" 2>/dev/null && : > \"$D/node-unsupported\" 2>/dev/null && printf '%s' '{\"systemMessage\":\"The impeccable design hook is not running: no Node 22 or newer on PATH. Install one, or remove the impeccable hook from your harness settings.\"}'; }; exit 0; }; } || node \".codex/skills/impeccable/scripts/hook.mjs\"", + "command": "[ ! -f \".codex/skills/impeccable/scripts/hook.mjs\" ] || ! node -e \"import('fs')\" 2>/dev/null || node \".codex/skills/impeccable/scripts/hook.mjs\"", "timeout": 30, "statusMessage": "Design deep pass" } diff --git a/.cursor/hooks.json b/.cursor/hooks.json index e4dd64f5e..c7dc57cea 100644 --- a/.cursor/hooks.json +++ b/.cursor/hooks.json @@ -3,7 +3,7 @@ "hooks": { "preToolUse": [ { - "command": "[ ! -f \".cursor/skills/impeccable/scripts/hook-before-edit.mjs\" ] || ! { node -e \"import('fs')\" 2>/dev/null || { D=\"$HOME/.impeccable\"; [ -f \"$D/node-unsupported\" ] || { mkdir -p \"$D\" 2>/dev/null && : > \"$D/node-unsupported\" 2>/dev/null && printf '%s' '{\"systemMessage\":\"The impeccable design hook is not running: no Node 22 or newer on PATH. Install one, or remove the impeccable hook from your harness settings.\"}'; }; exit 0; }; } || node \".cursor/skills/impeccable/scripts/hook-before-edit.mjs\"", + "command": "[ ! -f \".cursor/skills/impeccable/scripts/hook-before-edit.mjs\" ] || ! node -e \"import('fs')\" 2>/dev/null || node \".cursor/skills/impeccable/scripts/hook-before-edit.mjs\"", "timeout": 5 } ] diff --git a/scripts/lib/transformers/hooks.js b/scripts/lib/transformers/hooks.js index 72d39e006..e92ae29d8 100644 --- a/scripts/lib/transformers/hooks.js +++ b/scripts/lib/transformers/hooks.js @@ -56,22 +56,30 @@ const CLAUDE_PROJECT_HOOK = '${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scr // or no node at all, kills the hook script while it is still being parsed, // before the script's own always-exit-0 contract can run. Nothing written in // ESM can report that, the doctor and the sub-commands included, so the command -// string reports it instead: probe dynamic import, and when the probe fails -// hand the user the two things they can act on (install a supported node, or -// drop the hook) through the universal `systemMessage` field on exit 0. A -// marker under ~/.impeccable keeps it to one notice per machine rather than one -// per edit. The message says `on PATH` deliberately: the common cause is a hook -// shell whose PATH misses the version manager, so a user already running Node 22 -// needs to know the hook's PATH is at issue and not their install. Apostrophes -// cannot appear in the message, which travels in a single-quoted shell string. -const NODE_NOTICE_TEXT = 'The impeccable design hook is not running: no Node 22 or newer on PATH. ' +// string carries the probe: dynamic import, and no-op at exit 0 when it fails. +// +// `notice` is the shell that reports the dead runtime to the user, and it is +// passed in rather than baked in because the wire format is per harness. Claude +// Code reads a `systemMessage` field off stdout on exit 0; what Codex and Cursor +// do with stdout they did not ask for is unconfirmed, so they take the probe +// alone and an unsupported runtime stays as quiet there as it was before the +// probe existed. Adding their shape later is one more argument at the call site. +const guardedNode = (hookPath, notice = '') => { + const probe = notice + ? `! { node -e "import('fs')" 2>/dev/null || { ${notice}; exit 0; }; }` + : `! node -e "import('fs')" 2>/dev/null`; + return `[ ! -f "${hookPath}" ] || ${probe} || node "${hookPath}"`; +}; +// The message says `on PATH` deliberately: the common cause is a hook shell whose +// PATH misses the version manager, so a user already running Node 22 needs to know +// the hook's PATH is at issue and not their install. Apostrophes cannot appear in +// it, since it travels inside a single-quoted shell string. The marker under +// ~/.impeccable holds it to one notice per machine rather than one per edit. +const CLAUDE_NODE_NOTICE_TEXT = 'The impeccable design hook is not running: no Node 22 or newer on PATH. ' + 'Install one, or remove the impeccable hook from your harness settings.'; -const NODE_NOTICE = 'D="$HOME/.impeccable"; [ -f "$D/node-unsupported" ] || ' +const CLAUDE_NODE_NOTICE = 'D="$HOME/.impeccable"; [ -f "$D/node-unsupported" ] || ' + '{ mkdir -p "$D" 2>/dev/null && : > "$D/node-unsupported" 2>/dev/null && ' - + `printf '%s' '{"systemMessage":"${NODE_NOTICE_TEXT}"}'; }`; -const guardedNode = (hookPath) => - `[ ! -f "${hookPath}" ] || ! { node -e "import('fs')" 2>/dev/null || { ${NODE_NOTICE}; exit 0; }; } ` - + `|| node "${hookPath}"`; + + `printf '%s' '{"systemMessage":"${CLAUDE_NODE_NOTICE_TEXT}"}'; }`; const CLAUDE_PLUGIN_HOOK = '${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/hook.mjs'; const CODEX_PLUGIN_HOOK = '${PLUGIN_ROOT}/skills/impeccable/scripts/hook.mjs'; // Codex reads project hooks from `.codex/hooks.json`, but the skill payload the @@ -97,14 +105,14 @@ export function buildClaudeSettingsManifest() { hooks: [ { type: 'command', - command: guardedNode(CLAUDE_PROJECT_HOOK), + command: guardedNode(CLAUDE_PROJECT_HOOK, CLAUDE_NODE_NOTICE), timeout: TIMEOUT_SECONDS, statusMessage: STATUS_MESSAGE, }, ], }, ], - Stop: [stopEntry(guardedNode(CLAUDE_PROJECT_HOOK))], + Stop: [stopEntry(guardedNode(CLAUDE_PROJECT_HOOK, CLAUDE_NODE_NOTICE))], }, }; } @@ -124,14 +132,14 @@ export function buildClaudePluginHooksManifest() { hooks: [ { type: 'command', - command: guardedNode(CLAUDE_PLUGIN_HOOK), + command: guardedNode(CLAUDE_PLUGIN_HOOK, CLAUDE_NODE_NOTICE), timeout: TIMEOUT_SECONDS, statusMessage: STATUS_MESSAGE, }, ], }, ], - Stop: [stopEntry(guardedNode(CLAUDE_PLUGIN_HOOK))], + Stop: [stopEntry(guardedNode(CLAUDE_PLUGIN_HOOK, CLAUDE_NODE_NOTICE))], }, }; }