hooks: two-tier design hook — immediate per-edit rules + full-set Stop deep pass

Eval evidence showed the per-edit PostToolUse stream fires overwhelmingly
on copy-level rules (em-dash-overuse ~97x/session) and measurably makes
models more conservative, while a full-detector pass at completion is what
actually fixes contrast/padding/glow. Split the hook accordingly:

- Per-edit (PostToolUse) now surfaces only IMMEDIATE_TIER_RULES: broken
  output (broken-image, text-overflow, clipped-overflow-container,
  body-text-viewport-edge), objective contrast/legibility failures
  (low-contrast, gray-on-color, tiny-text), single-property mechanical
  slop (gradient-text, dark-glow), and design-system drift (the four
  design-system-* rules, which compound if left uncorrected). Everything
  else defers. Override with hook.perEditRules: "all" in
  .impeccable/config.json. Tiering is off for Cursor/Copilot harnesses,
  which have no Stop pass wired, so nothing gets silently dropped there.

- Stop deep pass (runStopHook): runs the FULL rule set over every UI file
  touched this session (tracked via the existing hook.cache.json session
  state; deferred-only edits now mark the file touched), dedupes against
  everything already surfaced per-edit, honors ignore-rule/file/value and
  inline disables, reuses the [impeccable@1] envelope, and no-ops fast
  when no UI files were touched. Emits hookSpecificOutput
  { hookEventName: "Stop", additionalContext } per the Claude Code SDK
  Stop contract (conversation continues so the model can act on it).
  Second Stop fire is silent - deep-pass findings are remembered.

- Wiring: Stop entries (timeout 30) in plugin/hooks/hooks.json, the
  .claude settings + .codex hooks manifests (transformers + hook-admin
  repair path). Claude Code and Codex both dispatch a native Stop event;
  Cursor's stop hook is inconsistently dispatched (pre-write gate stays)
  and Copilot's agentStop/sessionEnd don't inject model context, so
  neither gets a Stop entry - documented in reference/hooks.md.

- Tests: tiering split/override/harness gating, Stop dedupe + silent
  no-touched-files + ignore machinery + kill switches; existing per-edit
  tests moved to immediate-tier rule ids. 181 tests green; smoke-tested
  the built dist skill end to end (glow surfaced per-edit, em-dash only
  at Stop, second Stop silent).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-11 16:46:00 -07:00
co-authored by Claude Fable 5
parent 8091f452d5
commit c3aba1e343
63 changed files with 4454 additions and 271 deletions
+25 -1
View File
@@ -23,6 +23,26 @@ export const IMPECCABLE_HOOK_COMMAND_MARKER = 'skills/impeccable/scripts/hook.mj
const TIMEOUT_SECONDS = 5;
const STATUS_MESSAGE = 'Checking UI changes';
// The Stop deep pass scans every UI file touched in the session with the
// full rule set, so it gets a longer budget than the single-file per-edit
// pass. Wired only for Claude Code and Codex, which both dispatch a native
// `Stop` hook event; Cursor's stop hook is not consistently dispatched and
// GitHub Copilot's stop-style events do not feed context back to the model.
const STOP_TIMEOUT_SECONDS = 30;
const STOP_STATUS_MESSAGE = 'Design deep pass';
function stopEntry(command) {
return {
hooks: [
{
type: 'command',
command,
timeout: STOP_TIMEOUT_SECONDS,
statusMessage: STOP_STATUS_MESSAGE,
},
],
};
}
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_PLUGIN_HOOK = '${PLUGIN_ROOT}/skills/impeccable/scripts/hook.mjs';
@@ -32,7 +52,7 @@ const GITHUB_PROJECT_HOOK = '$(git rev-parse --show-toplevel)/.github/skills/imp
export function buildClaudeSettingsManifest() {
return {
description: 'Impeccable design detector: runs after Edit/Write/MultiEdit on UI files and surfaces findings as system reminders.',
description: 'Impeccable design detector: immediate-tier checks after Edit/Write/MultiEdit on UI files, full-rule deep pass on Stop.',
hooks: {
PostToolUse: [
{
@@ -47,6 +67,7 @@ export function buildClaudeSettingsManifest() {
],
},
],
Stop: [stopEntry(`node "${CLAUDE_PROJECT_HOOK}"`)],
},
};
}
@@ -73,6 +94,7 @@ export function buildClaudePluginHooksManifest() {
],
},
],
Stop: [stopEntry(`node "${CLAUDE_PLUGIN_HOOK}"`)],
},
};
}
@@ -96,6 +118,7 @@ export function buildCodexPluginHooksManifest() {
],
},
],
Stop: [stopEntry(`node "${CODEX_PLUGIN_HOOK}"`)],
},
};
}
@@ -116,6 +139,7 @@ export function buildCodexHooksManifest() {
],
},
],
Stop: [stopEntry(`node "${CODEX_PROJECT_HOOK}"`)],
},
};
}