Build: ship the launcher instead of bundling the JS engine

readSourceFiles no longer copies cli/engine into the skill; the scripts
payload is the launcher (executable bit preserved through dist, plugin/, and
universal.zip), impeccable.cmd, VERSION (synced from ENGINE_VERSION on every
build), the page JS, and command-metadata.json. Hook manifests call
`<scripts>/impeccable hook` behind an existence guard (Codex adds a
commandWindows sibling calling impeccable.cmd; Cursor runs hook-before-edit;
GitHub keeps the git rev-parse form; Grok mirrors Claude); the Node probe and
systemMessage notice are gone. build:release fetches the pinned engine for
every target (lenient) and stages bin/<os-arch>/ into the dist skill copies
after root harness dirs and plugin/ were synced, so git-delivered trees stay
launcher-only. The detection-rule count check reads the vendored
extension/detector/antipatterns.json and is skipped when absent.
build:browser is a stub; the codex prefix rewrite leaves
`{{scripts_path}}/impeccable` alone.

Prepared with AI assistance (Claude Code).
This commit is contained in:
Paul Bakaus
2026-08-31 19:57:32 -07:00
parent dac3f77d89
commit 11a1ea64a6
13 changed files with 336 additions and 1348 deletions
+7 -1
View File
@@ -1,3 +1,4 @@
import fs from 'fs';
import path from 'path';
import {
cleanDir,
@@ -359,7 +360,12 @@ export function createTransformer(config) {
ensureDir(scriptsOutDir);
for (const script of skill.scripts) {
const scriptContent = replaceScriptProviderMarker(script.content, placeholderKey, provider);
writeFile(path.join(scriptsOutDir, script.name), scriptContent);
const outPath = path.join(scriptsOutDir, script.name);
writeFile(outPath, scriptContent);
// The launcher must stay executable in every provider copy; a
// plain write would drop the bit and `impeccable context` would
// fail with EACCES on the first session.
if (script.mode) fs.chmodSync(outPath, script.mode);
scriptCount++;
}
}
+60 -98
View File
@@ -22,7 +22,7 @@
* correct wherever Claude Code unpacks the plugin.
*/
export const IMPECCABLE_HOOK_COMMAND_MARKER = 'skills/impeccable/scripts/hook.mjs';
export const IMPECCABLE_HOOK_COMMAND_MARKER = 'skills/impeccable/scripts/impeccable';
const TIMEOUT_SECONDS = 5;
const STATUS_MESSAGE = 'Checking UI changes';
@@ -34,12 +34,37 @@ const STATUS_MESSAGE = 'Checking UI changes';
const STOP_TIMEOUT_SECONDS = 30;
const STOP_STATUS_MESSAGE = 'Design deep pass';
function stopEntry(command) {
// The hook is a verb of the impeccable launcher that ships in the skill's
// scripts dir: `<scripts>/impeccable hook` (per-edit and Stop passes) and
// `<scripts>/impeccable hook-before-edit` (Cursor's preToolUse). The launcher
// runs the platform binary next to it, or downloads it once; no runtime probe
// is needed and there is no Node on the path to check.
export const LAUNCHER_NAME = 'impeccable';
export const LAUNCHER_NAME_WINDOWS = 'impeccable.cmd';
// A hook manifest can be copied into a user-level settings file (issue #399:
// user-level hooks fire in every project, where a project-relative path may
// not exist). Guard the invocation so a missing launcher exits 0 without
// swallowing the hook's real exit code when it is present: the `[ ! -f X ] ||
// X verb` form (not `... || true`) preserves the launcher's exit code, so
// Claude's exit-2 blocking signal still reaches the agent.
export const guardedLauncher = (launcherPath, verb = 'hook') =>
`[ ! -f "${launcherPath}" ] || "${launcherPath}" ${verb}`;
// cmd.exe form for harnesses that read a `commandWindows` sibling (Codex
// 0.146.0+ selects it on Windows; issue #452). `exit /b` forwards the
// launcher's errorlevel. Paths keep forward slashes; cmd.exe accepts them in
// quoted paths and it is the form the CLI already writes.
export const windowsLauncherCommand = (launcherCmdPath, verb = 'hook') =>
`if exist "${launcherCmdPath}" ("${launcherCmdPath}" ${verb} & exit /b)`;
function stopEntry(command, commandWindows) {
return {
hooks: [
{
type: 'command',
command,
...(commandWindows ? { commandWindows } : {}),
timeout: STOP_TIMEOUT_SECONDS,
statusMessage: STOP_STATUS_MESSAGE,
},
@@ -47,50 +72,31 @@ function stopEntry(command) {
};
}
const CLAUDE_PROJECT_HOOK = '${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs';
// The Node major the hook runtime requires, kept equal to the engines floor in
// package.json. The probe and the notice both derive from it so they cannot
// disagree about the supported version.
const NODE_MAJOR_FLOOR = 22;
// A hook manifest can be copied into a user-level settings file (issue #399:
// user-level hooks fire in every project, where a project-relative path may
// not exist). Guard node invocations so a missing file exits 0 without
// swallowing node's real exit code when the file is present.
//
// The runtime is guarded too (issue #410): a `node` on PATH too old for the
// hook's ESM syntax dies while hook.mjs is still being parsed, before the
// script's own always-exit-0 contract can run, so the harness reported a hook
// error on every edit and every Stop. Nothing written in ESM can report that
// condition, so the command string itself checks the version floor first, in
// ES5-only syntax that parses on any node old enough to fail it, and exits 0
// when the runtime is unsupported or missing.
//
// `notice` reports the dead runtime to the user. It is passed per harness
// because only some have a channel for it, checked against each harness's own
// hook reference on the events we hook:
// Claude Code / Codex: `systemMessage` on stdout is shown to the user -> notice
// Cursor: preToolUse output is permission-shaped and its `user_message`
// renders only on DENY, so warning would block the edit -> probe only
// Grok Build: PostToolUse stdout is ignored; Stop additionalContext
// reaches the model, but the node-version notice has no systemMessage
// channel on this harness -> probe only
// Copilot: output contract unconfirmed; do not guess a shape -> probe only
//
// The clamp avoids `<` and `>` deliberately: Volta's Windows shims run through
// `cmd /C`, which reads an angle bracket in the `-e` payload as redirection, so
// `>=` failed before node ran at all and the guard reported a missing runtime on
// a machine that had a supported one (volta-cli/volta#1791). Newlines break the
// same way, so this payload also has to stay on one line.
const NODE_PROBE = `node -e "process.exit(Math.min(parseInt(process.versions.node,10),${NODE_MAJOR_FLOOR})===${NODE_MAJOR_FLOOR}?0:1)" 2>/dev/null`;
const guardedNode = (hookPath, notice = '') => {
const probe = notice
? `! { ${NODE_PROBE} || { ${notice}; exit 0; }; }`
: `! ${NODE_PROBE}`;
return `[ ! -f "${hookPath}" ] || ${probe} || node "${hookPath}"`;
};
const launcherIn = (scriptsDir) => `${scriptsDir}/${LAUNCHER_NAME}`;
const launcherCmdIn = (scriptsDir) => `${scriptsDir}/${LAUNCHER_NAME_WINDOWS}`;
function buildClaudeCompatibleHooks(matcher, hookPath, notice = '') {
const command = guardedNode(hookPath, notice);
const CLAUDE_PROJECT_SCRIPTS = '${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts';
const CLAUDE_PLUGIN_SCRIPTS = '${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts';
const CODEX_PLUGIN_SCRIPTS = '${PLUGIN_ROOT}/skills/impeccable/scripts';
// Codex reads project hooks from `.codex/hooks.json`, but the skill payload the
// hook invokes lives under the install's own skills dir: a `.codex`-directory
// install keeps it at `.codex/skills/...`, while a `.agents` (Codex repo-skills)
// install keeps it at `.agents/skills/...`. Derive the path from the install dir
// so each generated manifest points at its own payload rather than a hardcoded
// `.agents`; otherwise the guarded hook silently no-ops on `.codex` installs.
const codexProjectScripts = (skillDir) => `${skillDir}/skills/impeccable/scripts`;
const CURSOR_SCRIPTS = '.cursor/skills/impeccable/scripts';
const GITHUB_PROJECT_SCRIPTS = '$(git rev-parse --show-toplevel)/.github/skills/impeccable/scripts';
// Grok project hooks are relative to the git/workspace root. Claude tool names
// in the matcher (Edit|Write|MultiEdit) alias to Grok's search_replace family.
const GROK_PROJECT_SCRIPTS = '.grok/skills/impeccable/scripts';
// `windows: true` adds the `commandWindows` sibling; only Codex-shaped
// consumers honor it, and an unknown key would fail Codex's strict parser if
// it were the other way round, so it stays opt-in per manifest.
function buildClaudeCompatibleHooks(matcher, scriptsDir, { windows = false } = {}) {
const command = guardedLauncher(launcherIn(scriptsDir));
const commandWindows = windows ? windowsLauncherCommand(launcherCmdIn(scriptsDir)) : undefined;
return {
PostToolUse: [
{
@@ -99,52 +105,21 @@ function buildClaudeCompatibleHooks(matcher, hookPath, notice = '') {
{
type: 'command',
command,
...(commandWindows ? { commandWindows } : {}),
timeout: TIMEOUT_SECONDS,
statusMessage: STATUS_MESSAGE,
},
],
},
],
Stop: [stopEntry(command)],
Stop: [stopEntry(command, commandWindows)],
};
}
// 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.
const NODE_NOTICE_TEXT = `The impeccable design hook is not running: no Node ${NODE_MAJOR_FLOOR} or newer on PATH. `
+ 'Install one, or remove the impeccable hook from your harness settings.';
// Claude Code and Codex both read `systemMessage`, so one payload serves both.
// The marker under ~/.impeccable holds it to one notice per machine (not per
// harness or per edit), and printf runs only after the marker write succeeds,
// so an unwritable HOME degrades to silence rather than a notice on every edit.
const SYSTEM_MESSAGE_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 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
// hook invokes lives under the install's own skills dir: a `.codex`-directory
// install keeps it at `.codex/skills/...`, while a `.agents` (Codex repo-skills)
// install keeps it at `.agents/skills/...`. Derive the path from the install dir
// so each generated manifest points at its own payload rather than a hardcoded
// `.agents` — otherwise the guarded hook silently no-ops on `.codex` installs.
const codexProjectHook = (skillDir) => `${skillDir}/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';
// Grok project hooks are relative to the git/workspace root. Claude tool names
// in the matcher (Edit|Write|MultiEdit) alias to Grok's search_replace family.
const GROK_PROJECT_HOOK = '.grok/skills/impeccable/scripts/hook.mjs';
export function buildClaudeSettingsManifest() {
return {
description: 'Impeccable design detector: immediate-tier checks after Edit/Write on UI files, full-rule deep pass on Stop.',
hooks: buildClaudeCompatibleHooks(
'Edit|Write',
CLAUDE_PROJECT_HOOK,
SYSTEM_MESSAGE_NOTICE,
),
hooks: buildClaudeCompatibleHooks('Edit|Write', CLAUDE_PROJECT_SCRIPTS),
};
}
@@ -156,11 +131,7 @@ export function buildClaudeSettingsManifest() {
// than `hooks`, failing the whole manifest (issue #330).
export function buildClaudePluginHooksManifest() {
return {
hooks: buildClaudeCompatibleHooks(
'Edit|Write',
CLAUDE_PLUGIN_HOOK,
SYSTEM_MESSAGE_NOTICE,
),
hooks: buildClaudeCompatibleHooks('Edit|Write', CLAUDE_PLUGIN_SCRIPTS),
};
}
@@ -169,11 +140,7 @@ export function buildClaudePluginHooksManifest() {
// instead of relying on its Claude compatibility alias.
export function buildCodexPluginHooksManifest() {
return {
hooks: buildClaudeCompatibleHooks(
'Edit|Write|apply_patch',
CODEX_PLUGIN_HOOK,
SYSTEM_MESSAGE_NOTICE,
),
hooks: buildClaudeCompatibleHooks('Edit|Write|apply_patch', CODEX_PLUGIN_SCRIPTS, { windows: true }),
};
}
@@ -181,13 +148,8 @@ export function buildCodexPluginHooksManifest() {
// emitted command points at that install's payload. Defaults to `.codex` for the
// Codex provider, whose self-consistent bundle keeps the skill at `.codex/skills`.
export function buildCodexHooksManifest(skillDir = '.codex') {
const hookPath = codexProjectHook(skillDir);
return {
hooks: buildClaudeCompatibleHooks(
'Edit|Write|apply_patch',
hookPath,
SYSTEM_MESSAGE_NOTICE,
),
hooks: buildClaudeCompatibleHooks('Edit|Write|apply_patch', codexProjectScripts(skillDir), { windows: true }),
};
}
@@ -197,7 +159,7 @@ export function buildCursorHooksManifest() {
hooks: {
preToolUse: [
{
command: guardedNode(CURSOR_BEFORE_EDIT_SCRIPT),
command: guardedLauncher(launcherIn(CURSOR_SCRIPTS), 'hook-before-edit'),
timeout: TIMEOUT_SECONDS,
},
],
@@ -224,7 +186,7 @@ export function buildGitHubHooksManifest() {
{
type: 'command',
matcher: 'edit|create|apply_patch',
bash: guardedNode(GITHUB_PROJECT_HOOK),
bash: guardedLauncher(launcherIn(GITHUB_PROJECT_SCRIPTS)),
timeoutSec: TIMEOUT_SECONDS,
},
],
@@ -239,7 +201,7 @@ export function buildGitHubHooksManifest() {
// https://docs.x.ai/build/features/hooks
export function buildGrokHooksManifest() {
return {
hooks: buildClaudeCompatibleHooks('Edit|Write|MultiEdit', GROK_PROJECT_HOOK),
hooks: buildClaudeCompatibleHooks('Edit|Write|MultiEdit', GROK_PROJECT_SCRIPTS),
};
}