mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Keep the unsupported-node notice to the harness that can render it
`systemMessage` on stdout is a Claude Code contract. The shared guard was emitting it for Codex and Cursor too, where what a harness does with stdout it did not ask for is unconfirmed, and a Cursor preToolUse hook printing an unexpected JSON object is the wrong thing to guess about. Pass the notice in per harness instead of baking it into the guard. Claude manifests opt in; Codex and Cursor take the runtime probe alone, so an unsupported runtime stays as quiet there as it was before the probe existed. Giving them their own shape later is one more argument at the call site. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0db59088ff
commit
8397d532b9
+2
-2
@@ -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"
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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))],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user