Merge remote-tracking branch 'upstream/main' into fix/detect-system-chrome-gpu-window

# Conflicts:
#	scripts/test-suites.mjs
This commit is contained in:
Vinaywho
2026-07-27 15:13:14 +05:30
769 changed files with 41365 additions and 3458 deletions
+1 -1
View File
@@ -400,7 +400,7 @@ function syncRootHookManifests(rootDir) {
const synced = [];
for (const config of Object.values(PROVIDERS)) {
if (!config.emitHooks) continue;
const manifest = hooksJsonFor(config.emitHooks);
const manifest = hooksJsonFor(config.emitHooks, { configDir: config.configDir });
if (!manifest) continue;
const rel = config.hooksManifestRel || path.join('hooks', 'hooks.json');
const dest = path.join(rootDir, config.configDir, rel);
+29 -1
View File
@@ -13,6 +13,12 @@ import {
import { SKILL_CATEGORIES, CATEGORY_ORDER } from '../skill-categories.js';
import { hooksJsonFor } from './hooks.js';
// Preamble prepended to every generated degraded-mode fallback reference file.
// These files are single-sourced from skill/agents/ so a harness with no
// subagent capability runs each role inline from the same specialized text.
const DEGRADED_PREAMBLE = `<!-- Generated from skill/agents/ at build time. Do not edit; edit the agent definition. -->
This harness has no subagent capability, so you are running this role inline. Step fully out of the work you just finished, adopt only this file's instructions for the pass, and disclose the substitution in one line when you report. Where the text below addresses a parent agent, you are both parties: produce the full output contract first, then act on it yourself.`;
/**
* Map from frontmatter field name to extraction spec.
*
@@ -258,6 +264,28 @@ export function createTransformer(config) {
}
}
// Generate degraded-mode fallback reference files from the shipped
// subagent definitions. Single-sourced from skill/agents/ so a harness
// with no subagent capability runs each role inline from the same
// specialized text. Role name = agent name minus the `impeccable-`
// prefix. These pass through the same provider-block compilation and
// placeholder replacement as ordinary reference files, so <codex> blocks
// and {{placeholders}} resolve identically.
if (skill.agents && skill.agents.length > 0) {
const degradedDir = path.join(skillDir, 'reference', 'degraded');
ensureDir(degradedDir);
for (const agent of skill.agents) {
const role = agent.name.replace(/^impeccable-/, '');
let body = compileProviderBlocks(agent.body, providerTags);
body = replacePlaceholders(body, placeholderKey, [], allSkillNames);
body = stripRuleMarkers(body);
body = body.replace(/\{\{scripts_path\}\}/g, scriptsPath);
const content = `${DEGRADED_PREAMBLE}\n\n${body.replace(/^\s+/, '')}`;
writeFile(path.join(degradedDir, `${role}.md`), content);
refCount++;
}
}
// Copy script files
if (skill.scripts && skill.scripts.length > 0) {
const scriptsOutDir = path.join(skillDir, 'scripts');
@@ -308,7 +336,7 @@ export function createTransformer(config) {
// `.codex/hooks.json`, and Cursor uses `.cursor/hooks.json`.
let hooksEmitted = false;
if (config.emitHooks) {
const manifest = hooksJsonFor(config.emitHooks);
const manifest = hooksJsonFor(config.emitHooks, { configDir });
if (manifest) {
const hooksRel = config.hooksManifestRel || path.join('hooks', 'hooks.json');
writeFile(path.join(providerDir, configDir, hooksRel), JSON.stringify(manifest, null, 2) + '\n');
+71 -16
View File
@@ -47,9 +47,60 @@ 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/Stop stdout is ignored outright -> probe only
// Copilot: output contract unconfirmed; do not guess a shape -> probe only
const NODE_PROBE = `node -e "process.exit(parseInt(process.versions.node,10)>=${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}"`;
};
// 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';
const CODEX_PROJECT_HOOK = '.agents/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
@@ -66,14 +117,14 @@ export function buildClaudeSettingsManifest() {
hooks: [
{
type: 'command',
command: `node "${CLAUDE_PROJECT_HOOK}"`,
command: guardedNode(CLAUDE_PROJECT_HOOK, SYSTEM_MESSAGE_NOTICE),
timeout: TIMEOUT_SECONDS,
statusMessage: STATUS_MESSAGE,
},
],
},
],
Stop: [stopEntry(`node "${CLAUDE_PROJECT_HOOK}"`)],
Stop: [stopEntry(guardedNode(CLAUDE_PROJECT_HOOK, SYSTEM_MESSAGE_NOTICE))],
},
};
}
@@ -93,14 +144,14 @@ export function buildClaudePluginHooksManifest() {
hooks: [
{
type: 'command',
command: `node "${CLAUDE_PLUGIN_HOOK}"`,
command: guardedNode(CLAUDE_PLUGIN_HOOK, SYSTEM_MESSAGE_NOTICE),
timeout: TIMEOUT_SECONDS,
statusMessage: STATUS_MESSAGE,
},
],
},
],
Stop: [stopEntry(`node "${CLAUDE_PLUGIN_HOOK}"`)],
Stop: [stopEntry(guardedNode(CLAUDE_PLUGIN_HOOK, SYSTEM_MESSAGE_NOTICE))],
},
};
}
@@ -117,19 +168,23 @@ export function buildCodexPluginHooksManifest() {
hooks: [
{
type: 'command',
command: `node "${CODEX_PLUGIN_HOOK}"`,
command: guardedNode(CODEX_PLUGIN_HOOK, SYSTEM_MESSAGE_NOTICE),
timeout: TIMEOUT_SECONDS,
statusMessage: STATUS_MESSAGE,
},
],
},
],
Stop: [stopEntry(`node "${CODEX_PLUGIN_HOOK}"`)],
Stop: [stopEntry(guardedNode(CODEX_PLUGIN_HOOK, SYSTEM_MESSAGE_NOTICE))],
},
};
}
export function buildCodexHooksManifest() {
// `skillDir` is the install's own dot-directory (a provider's configDir), so the
// 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: {
PostToolUse: [
@@ -138,14 +193,14 @@ export function buildCodexHooksManifest() {
hooks: [
{
type: 'command',
command: `node "${CODEX_PROJECT_HOOK}"`,
command: guardedNode(hookPath, SYSTEM_MESSAGE_NOTICE),
timeout: TIMEOUT_SECONDS,
statusMessage: STATUS_MESSAGE,
},
],
},
],
Stop: [stopEntry(`node "${CODEX_PROJECT_HOOK}"`)],
Stop: [stopEntry(guardedNode(hookPath, SYSTEM_MESSAGE_NOTICE))],
},
};
}
@@ -156,7 +211,7 @@ export function buildCursorHooksManifest() {
hooks: {
preToolUse: [
{
command: `node "${CURSOR_BEFORE_EDIT_SCRIPT}"`,
command: guardedNode(CURSOR_BEFORE_EDIT_SCRIPT),
timeout: TIMEOUT_SECONDS,
},
],
@@ -183,7 +238,7 @@ export function buildGitHubHooksManifest() {
{
type: 'command',
matcher: 'edit|create|apply_patch',
bash: `node "${GITHUB_PROJECT_HOOK}"`,
bash: guardedNode(GITHUB_PROJECT_HOOK),
timeoutSec: TIMEOUT_SECONDS,
},
],
@@ -205,24 +260,24 @@ export function buildGrokHooksManifest() {
hooks: [
{
type: 'command',
command: `node "${GROK_PROJECT_HOOK}"`,
command: guardedNode(GROK_PROJECT_HOOK),
timeout: TIMEOUT_SECONDS,
statusMessage: STATUS_MESSAGE,
},
],
},
],
Stop: [stopEntry(`node "${GROK_PROJECT_HOOK}"`)],
Stop: [stopEntry(guardedNode(GROK_PROJECT_HOOK))],
},
};
}
export function hooksJsonFor(provider) {
export function hooksJsonFor(provider, options = {}) {
switch (provider) {
case 'claude':
return buildClaudeSettingsManifest();
case 'codex':
return buildCodexHooksManifest();
return buildCodexHooksManifest(options.configDir || '.codex');
case 'cursor':
return buildCursorHooksManifest();
case 'github':
+20
View File
@@ -211,6 +211,26 @@ try {
}
console.log(`\n${cfg.label} ${version} released as ${tag}`);
// npx impeccable update serves from impeccable.style, not from this release:
// the site must be redeployed (its deploy overlays public main first). Warn
// loudly when the served version lags so a release never silently strands
// update users on old content again (the 4.0.0 release did exactly that).
if (component === 'skill' && !dryRun) {
try {
const res = await fetch('https://impeccable.style/api/version');
const served = (await res.json()).skills;
if (served === version) {
console.log(`✓ impeccable.style serves ${served}`);
} else {
console.log(`\n⚠ impeccable.style still serves ${served}, not ${version}.`);
console.log(' npx impeccable update users get the OLD version until the site redeploys:');
console.log(' cd ../impeccable-site && bun run deploy');
}
} catch {
console.log('⚠ could not reach impeccable.style/api/version to verify the served bundle');
}
}
if (cfg.postReleaseHint) {
console.log(`\n→ Next step: ${cfg.postReleaseHint}`);
}
+22 -1
View File
@@ -6,6 +6,7 @@ export const OPT_IN_SUITES = [
'cli-remote-e2e',
'live-e2e',
'live-e2e-accept-cleanup',
'new-work-e2e',
'skill-behavior',
'live-svelte-adapter-deepseek',
];
@@ -86,7 +87,7 @@ export const SUITES = {
/^scripts\/(benchmark-detector|build-browser-detector|build-extension)\.js$/,
/^site\/(pages\/detector|public\/antipattern|data\/anti-patterns-catalog\.js)/,
/^tests\/design-system\.test\.mjs$/,
/^tests\/(detect-antipatterns|detect-url-launch|inline-ignores|extension-build|fixtures\/antipatterns)/,
/^tests\/(detect-antipatterns|detect-cli-design-contamination|detect-url-launch|inline-ignores|extension-build|fixtures\/antipatterns)/,
],
commands: [
{
@@ -105,6 +106,7 @@ export const SUITES = {
'tests/design-system.test.mjs',
'tests/detect-antipatterns-fixtures.test.mjs',
'tests/detect-antipatterns-browser.test.mjs',
'tests/detect-cli-design-contamination.test.mjs',
],
},
],
@@ -153,6 +155,7 @@ export const SUITES = {
'tests/live-session-store.test.mjs',
'tests/live-source-lock.test.mjs',
'tests/live-source-search.test.mjs',
'tests/live-tanstack-adapter.test.mjs',
'tests/live-target-context.test.mjs',
'tests/live-wrap.test.mjs',
'tests/live-wrap-buffer-aware.test.mjs',
@@ -222,6 +225,24 @@ export const SUITES = {
},
],
},
'new-work-e2e': {
description: 'Playwright smoke sweep of the new-work concept/serve-question decision page plus the offline fake image generator.',
optIn: true,
needsPlaywright: true,
triggers: [
...COMMON_INFRA_PATTERNS,
/^skill\/scripts\/(serve-question|generate-image|concept-seed)\.mjs$/,
/^tests\/new-work-e2e(\.test\.mjs|\/)/,
],
commands: [
{
runner: 'node',
timeoutMs: 600000,
forceExit: true,
files: ['tests/new-work-e2e.test.mjs'],
},
],
},
'live-e2e-accept-cleanup': {
description: 'Provider-backed post-accept cleanup regression.',
optIn: true,