diff --git a/.agents/skills/impeccable/scripts/context-signals.mjs b/.agents/skills/impeccable/scripts/context-signals.mjs index 973b76d0c..2fc27bea7 100644 --- a/.agents/skills/impeccable/scripts/context-signals.mjs +++ b/.agents/skills/impeccable/scripts/context-signals.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Context-signals gatherer for the bare `$impeccable` + * Context-signals gatherer for the bare Impeccable invocation * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.agents/skills/impeccable/scripts/context.mjs b/.agents/skills/impeccable/scripts/context.mjs index c050e4a04..11f2aabe0 100644 --- a/.agents/skills/impeccable/scripts/context.mjs +++ b/.agents/skills/impeccable/scripts/context.mjs @@ -23,6 +23,7 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseTargetOptions } from './lib/target-args.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; @@ -902,7 +903,7 @@ async function cli() { 'or wording that clearly maps to a from-scratch build/shape flow, load ' + 'reference/init.md and write PRODUCT.md first; for any other (scoped) ' + 'command against existing code, proceed using the code as context and ' + - 'offer `$impeccable init` as a suggestion (do not block).', + `offer \`${IMPECCABLE_COMMAND} init\` as a suggestion (do not block).`, ]; parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists })); if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) { diff --git a/.agents/skills/impeccable/scripts/critique-storage.mjs b/.agents/skills/impeccable/scripts/critique-storage.mjs index 41bde465b..6b4d225cf 100644 --- a/.agents/skills/impeccable/scripts/critique-storage.mjs +++ b/.agents/skills/impeccable/scripts/critique-storage.mjs @@ -2,11 +2,11 @@ /** * Critique persistence helper. * - * Each run of $impeccable critique writes a per-target snapshot to + * Each critique run writes a per-target snapshot to * .impeccable/critique/__.md * with a small YAML frontmatter carrying the score + P0/P1 counts. * - * $impeccable polish reads the latest matching snapshot at start as its + * The polish workflow reads the latest matching snapshot at start as its * fix backlog. No other skill auto-reads critique output. * * The slug is derived mechanically from the *resolved* primary artifact diff --git a/.agents/skills/impeccable/scripts/hook-admin.mjs b/.agents/skills/impeccable/scripts/hook-admin.mjs index 5c727f514..8b37b1f3b 100644 --- a/.agents/skills/impeccable/scripts/hook-admin.mjs +++ b/.agents/skills/impeccable/scripts/hook-admin.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * `$impeccable hooks ` — manage the design hook runtime + * The Impeccable hooks command manages the design hook runtime * via the `hook` key and shared detector ignores via the `detector` key in * .impeccable/config.json / .impeccable/config.local.json. * @@ -21,6 +21,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; import { getConfigPath, @@ -184,7 +185,7 @@ function writeHookConfig(cwd, hookConfig, opts = {}) { const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {}; const existingHook = stripDetectorKeys(hookSection(existing)); // Merge over the existing hook object so fields the merge helpers don't manage - // (consent, quiet, auditLog) survive a `$impeccable hooks` edit. + // (consent, quiet, auditLog) survive an Impeccable hooks edit. const next = { ...existing, hook: { ...existingHook, ...hookConfig } }; fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n'); @@ -513,9 +514,9 @@ function parseIgnoreRuleArgs(args) { function addIgnoreRule(cwd, args) { const parsed = parseIgnoreRuleArgs(args); const rule = parsed.rule; - if (!rule) throw new Error('Pass a rule id, e.g. $impeccable hooks ignore-rule side-tab'); + if (!rule) throw new Error(`Pass a rule id, e.g. ${IMPECCABLE_COMMAND} hooks ignore-rule side-tab`); if (rule === 'overused-font' && !parsed.allValues) { - throw new Error('overused-font is value-specific by default. Use $impeccable hooks ignore-value overused-font for a confirmed font, or $impeccable hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.'); + throw new Error(`overused-font is value-specific by default. Use ${IMPECCABLE_COMMAND} hooks ignore-value overused-font for a confirmed font, or ${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.`); } const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule); @@ -524,7 +525,7 @@ function addIgnoreRule(cwd, args) { } function addIgnoreFile(cwd, glob) { - if (!glob) throw new Error('Pass a glob, e.g. $impeccable hooks ignore-file "src/legacy/**"'); + if (!glob) throw new Error(`Pass a glob, e.g. ${IMPECCABLE_COMMAND} hooks ignore-file "src/legacy/**"`); const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob); writeDetectorConfig(cwd, config); @@ -569,7 +570,7 @@ function parseIgnoreValueArgs(args) { function addIgnoreValue(cwd, args) { const parsed = parseIgnoreValueArgs(args); if (!parsed.rule || !parsed.value) { - throw new Error('Pass a rule id and value, e.g. $impeccable hooks ignore-value overused-font Inter'); + throw new Error(`Pass a rule id and value, e.g. ${IMPECCABLE_COMMAND} hooks ignore-value overused-font Inter`); } if (parsed.shared && parsed.local) { diff --git a/.agents/skills/impeccable/scripts/hook-lib.mjs b/.agents/skills/impeccable/scripts/hook-lib.mjs index f5cca583f..0d9722953 100644 --- a/.agents/skills/impeccable/scripts/hook-lib.mjs +++ b/.agents/skills/impeccable/scripts/hook-lib.mjs @@ -41,6 +41,7 @@ import os from 'node:os'; import path from 'node:path'; import { pathToFileURL, fileURLToPath } from 'node:url'; import { extractPlatform, loadContext } from './context.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -661,7 +662,7 @@ export function bumpEditCount(cache, sessionId, filePath) { } export function suppressionNotice(filePath) { - return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run $impeccable audit to revisit.`; + return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run ${IMPECCABLE_COMMAND} audit to revisit.`; } // Glob → RegExp. Supports `**`, `*`, `?`, and `{a,b}` alternation. @@ -877,7 +878,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) { const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`; const lines = shown.map((f) => formatFindingLine(f)); const more = remaining > 0 - ? `... and ${remaining} more (see $impeccable audit).` + ? `... and ${remaining} more (see ${IMPECCABLE_COMMAND} audit).` : null; const footer = directiveFooter(display); @@ -921,7 +922,7 @@ function renderGroupedTemplate(groups, config, opts = {}) { shownCount += shown.length; const hidden = group.findings.length - shown.length; if (hidden > 0) { - lines.push(`- ... ${hidden} more in ${display} (see $impeccable audit).`); + lines.push(`- ... ${hidden} more in ${display} (see ${IMPECCABLE_COMMAND} audit).`); } } @@ -937,7 +938,7 @@ function clampGroupedToBudget(header, lines, footer, maxChars) { const assemble = (linesArr, omitted) => [ header, ...linesArr, - ...(omitted ? ['... and more (see $impeccable audit).'] : []), + ...(omitted ? [`... and more (see ${IMPECCABLE_COMMAND} audit).`] : []), '', footer, ].join('\n'); @@ -970,7 +971,7 @@ function clampToBudget(header, lines, more, footer, maxChars) { let assembled = assemble(working, moreText); while (assembled.length > maxChars && working.length > 1) { working.pop(); - moreText = '... and more (see $impeccable audit).'; + moreText = `... and more (see ${IMPECCABLE_COMMAND} audit).`; assembled = assemble(working, moreText); } if (assembled.length > maxChars) { @@ -1002,7 +1003,7 @@ function formatFindingIgnoreCommand(finding) { const value = extractFindingIgnoreValueRaw(finding); const valueArg = quoteCommandArg(value); const reason = quoteCommandArg(`User confirmed ${value} is intentional`); - return `$impeccable hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; + return `${IMPECCABLE_COMMAND} hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; } function quoteCommandArg(value) { @@ -1447,7 +1448,7 @@ export function designSystemOptions(config, detector, projectCwd) { export function appendDesignSystemNote(text, scanOptions) { if (!text || !scanOptions?.designSystem?.mdNewerThanJson) return text; - return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run $impeccable document to refresh the design-system sidecar.`; + return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the design-system sidecar.`; } // The directive footer is the part of the hook output that steers model @@ -1464,16 +1465,16 @@ export function appendDesignSystemNote(text, scanOptions) { // raw envelope. Asking the model to surface the resolution in its // reply is the cheapest way to make the feedback loop visible. function directiveFooter(display, opts = {}) { - const ignoreFileCommand = `$impeccable hooks ignore-file ${quoteCommandArg(display)}`; + const ignoreFileCommand = `${IMPECCABLE_COMMAND} hooks ignore-file ${quoteCommandArg(display)}`; const fileIgnoreGuidance = opts.grouped - ? 'run `$impeccable hooks ignore-file ` for the specific file' + ? `run \`${IMPECCABLE_COMMAND} hooks ignore-file \` for the specific file` : `run \`${ignoreFileCommand}\``; return [ 'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.', '', 'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.', '', - `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`$impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`$impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`$impeccable hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run $impeccable audit for the full pass.`, + `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`${IMPECCABLE_COMMAND} hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`${IMPECCABLE_COMMAND} hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run ${IMPECCABLE_COMMAND} audit for the full pass.`, ].join('\n'); } diff --git a/.agents/skills/impeccable/scripts/lib/impeccable-paths.mjs b/.agents/skills/impeccable/scripts/lib/impeccable-paths.mjs index 91121dd59..2ccbe7b74 100644 --- a/.agents/skills/impeccable/scripts/lib/impeccable-paths.mjs +++ b/.agents/skills/impeccable/scripts/lib/impeccable-paths.mjs @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { resolveProjectRoot } from '../context.mjs'; +export { IMPECCABLE_COMMAND_PREFIX } from './provider.mjs'; export const IMPECCABLE_DIR = '.impeccable'; export const LIVE_DIR = 'live'; diff --git a/.agents/skills/impeccable/scripts/lib/provider.mjs b/.agents/skills/impeccable/scripts/lib/provider.mjs new file mode 100644 index 000000000..57d2797b0 --- /dev/null +++ b/.agents/skills/impeccable/scripts/lib/provider.mjs @@ -0,0 +1,4 @@ +// Source scripts default to slash commands. The provider build replaces only +// this exact declaration, avoiding heuristic rewrites across executable code. +export const IMPECCABLE_COMMAND_PREFIX = "$"; +export const IMPECCABLE_COMMAND = `${IMPECCABLE_COMMAND_PREFIX}impeccable`; diff --git a/.agents/skills/impeccable/scripts/live-browser.js b/.agents/skills/impeccable/scripts/live-browser.js index a9e0d5e0f..616f1290a 100644 --- a/.agents/skills/impeccable/scripts/live-browser.js +++ b/.agents/skills/impeccable/scripts/live-browser.js @@ -57,6 +57,7 @@ const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 }; const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint const PREFIX = 'impeccable-live'; + const IMPECCABLE_COMMAND = (window.__IMPECCABLE_COMMAND_PREFIX__ || '/') + 'impeccable'; const PICK_CURSOR_STYLE_ID = PREFIX + '-pick-cursor-style'; const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000; const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({ @@ -6145,7 +6146,7 @@ switch (msg.type) { case 'connected': hasProjectContext = !!msg.hasProjectContext; - if (!hasProjectContext) showToast('No PRODUCT.md found. Variants will be brand-agnostic. Run $impeccable init to generate one.', 7000); + if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000); console.log('[impeccable] Live mode connected.'); syncAgentPollingUi(!!msg.agentPolling); startAgentStatusPoll(); @@ -10538,7 +10539,7 @@ void main() { if (designState.present === false) { const empty = document.createElement('div'); empty.className = 'empty'; - empty.innerHTML = `No DESIGN.md yetCreate one by running $impeccable document in your terminal, then re-open this panel.`; + empty.innerHTML = `No DESIGN.md yetCreate one by running ${IMPECCABLE_COMMAND} document in your terminal, then re-open this panel.`; body.appendChild(empty); return; } @@ -10568,7 +10569,7 @@ void main() { box.className = 'stale'; box.innerHTML = ` - DESIGN.md is newer than .impeccable/design.json. Run $impeccable document to refresh the sidecar. + DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the sidecar. `; return box; } @@ -10576,7 +10577,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running $impeccable document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running ${IMPECCABLE_COMMAND} document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/.agents/skills/impeccable/scripts/live-server.mjs b/.agents/skills/impeccable/scripts/live-server.mjs index 27005ef3c..5735f2aec 100644 --- a/.agents/skills/impeccable/scripts/live-server.mjs +++ b/.agents/skills/impeccable/scripts/live-server.mjs @@ -36,6 +36,7 @@ import { getDesignSidecarPath, getLiveDir, getLiveAnnotationsDir, + IMPECCABLE_COMMAND_PREFIX, readLiveServerInfo, removeLiveServerInfo, resolveDesignSidecarPath, @@ -413,6 +414,7 @@ function createRequestHandler({ detectScript, liveScriptParts }) { token: state.token, port: state.port, vocabulary: LIVE_COMMANDS, + commandPrefix: IMPECCABLE_COMMAND_PREFIX, parts, }); res.writeHead(200, { diff --git a/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs b/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs index 9229e34f8..b77f6a542 100644 --- a/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.agents/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -32,10 +32,11 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { +export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', parts }) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + + `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; diff --git a/.agents/skills/impeccable/scripts/pin.mjs b/.agents/skills/impeccable/scripts/pin.mjs index e5aaadad3..52ea2701b 100644 --- a/.agents/skills/impeccable/scripts/pin.mjs +++ b/.agents/skills/impeccable/scripts/pin.mjs @@ -6,7 +6,7 @@ * node /pin.mjs pin * node /pin.mjs unpin * - * `pin audit` creates a lightweight /audit skill that redirects to $impeccable audit. + * `pin audit` creates a lightweight audit skill that redirects to Impeccable's audit workflow. * `unpin audit` removes that shortcut. * * The script discovers harness directories (.claude/skills, .cursor/skills, etc.) @@ -14,7 +14,7 @@ */ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs'; -import { join, resolve, dirname } from 'node:path'; +import { basename, join, resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -25,6 +25,8 @@ const HARNESS_DIRS = [ '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', ]; +const CODEX_HARNESSES = new Set(['.codex', '.agents']); + // Valid sub-command names const VALID_COMMANDS = [ 'craft', 'init', 'extract', 'document', 'shape', @@ -87,8 +89,12 @@ function loadCommandMetadata() { /** * Generate a pinned skill's SKILL.md content. */ -function generatePinnedSkill(command, metadata) { - const desc = metadata[command]?.description || `Shortcut for $impeccable ${command}.`; +function commandPrefixForSkillsDir(skillsDir) { + return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; +} + +function generatePinnedSkill(command, metadata, commandPrefix) { + const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; return `--- @@ -100,9 +106,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`$impeccable ${command}\`. +This is a pinned shortcut for \`${commandPrefix}impeccable ${command}\`. -Invoke $impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } @@ -118,10 +124,11 @@ function pin(command, projectRoot) { return false; } - const content = generatePinnedSkill(command, metadata); let created = 0; for (const skillsDir of harnessDirs) { + const commandPrefix = commandPrefixForSkillsDir(skillsDir); + const content = generatePinnedSkill(command, metadata, commandPrefix); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { @@ -143,7 +150,7 @@ function pin(command, projectRoot) { if (created > 0) { console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`); - console.log(`You can now use /${command} directly.`); + console.log('Use the pinned command directly in each harness.'); } return created > 0; @@ -177,7 +184,7 @@ function unpin(command, projectRoot) { if (removed > 0) { console.log(`\nUnpinned '${command}' from ${removed} location(s).`); - console.log(`Use $impeccable ${command} to access it.`); + console.log(`Use Impeccable's '${command}' workflow directly to access it.`); } else { console.log(`No pinned '${command}' shortcut found.`); } diff --git a/.claude/skills/impeccable/scripts/context-signals.mjs b/.claude/skills/impeccable/scripts/context-signals.mjs index e12881b81..2fc27bea7 100644 --- a/.claude/skills/impeccable/scripts/context-signals.mjs +++ b/.claude/skills/impeccable/scripts/context-signals.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Context-signals gatherer for the bare `/impeccable` + * Context-signals gatherer for the bare Impeccable invocation * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.claude/skills/impeccable/scripts/context.mjs b/.claude/skills/impeccable/scripts/context.mjs index 4530f41d5..11f2aabe0 100644 --- a/.claude/skills/impeccable/scripts/context.mjs +++ b/.claude/skills/impeccable/scripts/context.mjs @@ -23,6 +23,7 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseTargetOptions } from './lib/target-args.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; @@ -902,7 +903,7 @@ async function cli() { 'or wording that clearly maps to a from-scratch build/shape flow, load ' + 'reference/init.md and write PRODUCT.md first; for any other (scoped) ' + 'command against existing code, proceed using the code as context and ' + - 'offer `/impeccable init` as a suggestion (do not block).', + `offer \`${IMPECCABLE_COMMAND} init\` as a suggestion (do not block).`, ]; parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists })); if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) { diff --git a/.claude/skills/impeccable/scripts/critique-storage.mjs b/.claude/skills/impeccable/scripts/critique-storage.mjs index 645628c8c..6b4d225cf 100644 --- a/.claude/skills/impeccable/scripts/critique-storage.mjs +++ b/.claude/skills/impeccable/scripts/critique-storage.mjs @@ -2,11 +2,11 @@ /** * Critique persistence helper. * - * Each run of /impeccable critique writes a per-target snapshot to + * Each critique run writes a per-target snapshot to * .impeccable/critique/__.md * with a small YAML frontmatter carrying the score + P0/P1 counts. * - * /impeccable polish reads the latest matching snapshot at start as its + * The polish workflow reads the latest matching snapshot at start as its * fix backlog. No other skill auto-reads critique output. * * The slug is derived mechanically from the *resolved* primary artifact diff --git a/.claude/skills/impeccable/scripts/hook-admin.mjs b/.claude/skills/impeccable/scripts/hook-admin.mjs index ce4ec7b2b..8b37b1f3b 100644 --- a/.claude/skills/impeccable/scripts/hook-admin.mjs +++ b/.claude/skills/impeccable/scripts/hook-admin.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * `/impeccable hooks ` — manage the design hook runtime + * The Impeccable hooks command manages the design hook runtime * via the `hook` key and shared detector ignores via the `detector` key in * .impeccable/config.json / .impeccable/config.local.json. * @@ -21,6 +21,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; import { getConfigPath, @@ -184,7 +185,7 @@ function writeHookConfig(cwd, hookConfig, opts = {}) { const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {}; const existingHook = stripDetectorKeys(hookSection(existing)); // Merge over the existing hook object so fields the merge helpers don't manage - // (consent, quiet, auditLog) survive a `/impeccable hooks` edit. + // (consent, quiet, auditLog) survive an Impeccable hooks edit. const next = { ...existing, hook: { ...existingHook, ...hookConfig } }; fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n'); @@ -513,9 +514,9 @@ function parseIgnoreRuleArgs(args) { function addIgnoreRule(cwd, args) { const parsed = parseIgnoreRuleArgs(args); const rule = parsed.rule; - if (!rule) throw new Error('Pass a rule id, e.g. /impeccable hooks ignore-rule side-tab'); + if (!rule) throw new Error(`Pass a rule id, e.g. ${IMPECCABLE_COMMAND} hooks ignore-rule side-tab`); if (rule === 'overused-font' && !parsed.allValues) { - throw new Error('overused-font is value-specific by default. Use /impeccable hooks ignore-value overused-font for a confirmed font, or /impeccable hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.'); + throw new Error(`overused-font is value-specific by default. Use ${IMPECCABLE_COMMAND} hooks ignore-value overused-font for a confirmed font, or ${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.`); } const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule); @@ -524,7 +525,7 @@ function addIgnoreRule(cwd, args) { } function addIgnoreFile(cwd, glob) { - if (!glob) throw new Error('Pass a glob, e.g. /impeccable hooks ignore-file "src/legacy/**"'); + if (!glob) throw new Error(`Pass a glob, e.g. ${IMPECCABLE_COMMAND} hooks ignore-file "src/legacy/**"`); const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob); writeDetectorConfig(cwd, config); @@ -569,7 +570,7 @@ function parseIgnoreValueArgs(args) { function addIgnoreValue(cwd, args) { const parsed = parseIgnoreValueArgs(args); if (!parsed.rule || !parsed.value) { - throw new Error('Pass a rule id and value, e.g. /impeccable hooks ignore-value overused-font Inter'); + throw new Error(`Pass a rule id and value, e.g. ${IMPECCABLE_COMMAND} hooks ignore-value overused-font Inter`); } if (parsed.shared && parsed.local) { diff --git a/.claude/skills/impeccable/scripts/hook-lib.mjs b/.claude/skills/impeccable/scripts/hook-lib.mjs index 4953b91ca..0d9722953 100644 --- a/.claude/skills/impeccable/scripts/hook-lib.mjs +++ b/.claude/skills/impeccable/scripts/hook-lib.mjs @@ -41,6 +41,7 @@ import os from 'node:os'; import path from 'node:path'; import { pathToFileURL, fileURLToPath } from 'node:url'; import { extractPlatform, loadContext } from './context.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -661,7 +662,7 @@ export function bumpEditCount(cache, sessionId, filePath) { } export function suppressionNotice(filePath) { - return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run /impeccable audit to revisit.`; + return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run ${IMPECCABLE_COMMAND} audit to revisit.`; } // Glob → RegExp. Supports `**`, `*`, `?`, and `{a,b}` alternation. @@ -877,7 +878,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) { const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`; const lines = shown.map((f) => formatFindingLine(f)); const more = remaining > 0 - ? `... and ${remaining} more (see /impeccable audit).` + ? `... and ${remaining} more (see ${IMPECCABLE_COMMAND} audit).` : null; const footer = directiveFooter(display); @@ -921,7 +922,7 @@ function renderGroupedTemplate(groups, config, opts = {}) { shownCount += shown.length; const hidden = group.findings.length - shown.length; if (hidden > 0) { - lines.push(`- ... ${hidden} more in ${display} (see /impeccable audit).`); + lines.push(`- ... ${hidden} more in ${display} (see ${IMPECCABLE_COMMAND} audit).`); } } @@ -937,7 +938,7 @@ function clampGroupedToBudget(header, lines, footer, maxChars) { const assemble = (linesArr, omitted) => [ header, ...linesArr, - ...(omitted ? ['... and more (see /impeccable audit).'] : []), + ...(omitted ? [`... and more (see ${IMPECCABLE_COMMAND} audit).`] : []), '', footer, ].join('\n'); @@ -970,7 +971,7 @@ function clampToBudget(header, lines, more, footer, maxChars) { let assembled = assemble(working, moreText); while (assembled.length > maxChars && working.length > 1) { working.pop(); - moreText = '... and more (see /impeccable audit).'; + moreText = `... and more (see ${IMPECCABLE_COMMAND} audit).`; assembled = assemble(working, moreText); } if (assembled.length > maxChars) { @@ -1002,7 +1003,7 @@ function formatFindingIgnoreCommand(finding) { const value = extractFindingIgnoreValueRaw(finding); const valueArg = quoteCommandArg(value); const reason = quoteCommandArg(`User confirmed ${value} is intentional`); - return `/impeccable hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; + return `${IMPECCABLE_COMMAND} hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; } function quoteCommandArg(value) { @@ -1447,7 +1448,7 @@ export function designSystemOptions(config, detector, projectCwd) { export function appendDesignSystemNote(text, scanOptions) { if (!text || !scanOptions?.designSystem?.mdNewerThanJson) return text; - return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the design-system sidecar.`; + return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the design-system sidecar.`; } // The directive footer is the part of the hook output that steers model @@ -1464,16 +1465,16 @@ export function appendDesignSystemNote(text, scanOptions) { // raw envelope. Asking the model to surface the resolution in its // reply is the cheapest way to make the feedback loop visible. function directiveFooter(display, opts = {}) { - const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`; + const ignoreFileCommand = `${IMPECCABLE_COMMAND} hooks ignore-file ${quoteCommandArg(display)}`; const fileIgnoreGuidance = opts.grouped - ? 'run `/impeccable hooks ignore-file ` for the specific file' + ? `run \`${IMPECCABLE_COMMAND} hooks ignore-file \` for the specific file` : `run \`${ignoreFileCommand}\``; return [ 'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.', '', 'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.', '', - `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`, + `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`${IMPECCABLE_COMMAND} hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`${IMPECCABLE_COMMAND} hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run ${IMPECCABLE_COMMAND} audit for the full pass.`, ].join('\n'); } diff --git a/.claude/skills/impeccable/scripts/lib/impeccable-paths.mjs b/.claude/skills/impeccable/scripts/lib/impeccable-paths.mjs index 91121dd59..2ccbe7b74 100644 --- a/.claude/skills/impeccable/scripts/lib/impeccable-paths.mjs +++ b/.claude/skills/impeccable/scripts/lib/impeccable-paths.mjs @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { resolveProjectRoot } from '../context.mjs'; +export { IMPECCABLE_COMMAND_PREFIX } from './provider.mjs'; export const IMPECCABLE_DIR = '.impeccable'; export const LIVE_DIR = 'live'; diff --git a/.claude/skills/impeccable/scripts/lib/provider.mjs b/.claude/skills/impeccable/scripts/lib/provider.mjs new file mode 100644 index 000000000..7fa928a9c --- /dev/null +++ b/.claude/skills/impeccable/scripts/lib/provider.mjs @@ -0,0 +1,4 @@ +// Source scripts default to slash commands. The provider build replaces only +// this exact declaration, avoiding heuristic rewrites across executable code. +export const IMPECCABLE_COMMAND_PREFIX = "/"; +export const IMPECCABLE_COMMAND = `${IMPECCABLE_COMMAND_PREFIX}impeccable`; diff --git a/.claude/skills/impeccable/scripts/live-browser.js b/.claude/skills/impeccable/scripts/live-browser.js index 02b8c8bcf..616f1290a 100644 --- a/.claude/skills/impeccable/scripts/live-browser.js +++ b/.claude/skills/impeccable/scripts/live-browser.js @@ -57,6 +57,7 @@ const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 }; const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint const PREFIX = 'impeccable-live'; + const IMPECCABLE_COMMAND = (window.__IMPECCABLE_COMMAND_PREFIX__ || '/') + 'impeccable'; const PICK_CURSOR_STYLE_ID = PREFIX + '-pick-cursor-style'; const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000; const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({ @@ -6145,7 +6146,7 @@ switch (msg.type) { case 'connected': hasProjectContext = !!msg.hasProjectContext; - if (!hasProjectContext) showToast('No PRODUCT.md found. Variants will be brand-agnostic. Run /impeccable init to generate one.', 7000); + if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000); console.log('[impeccable] Live mode connected.'); syncAgentPollingUi(!!msg.agentPolling); startAgentStatusPoll(); @@ -10538,7 +10539,7 @@ void main() { if (designState.present === false) { const empty = document.createElement('div'); empty.className = 'empty'; - empty.innerHTML = `No DESIGN.md yetCreate one by running /impeccable document in your terminal, then re-open this panel.`; + empty.innerHTML = `No DESIGN.md yetCreate one by running ${IMPECCABLE_COMMAND} document in your terminal, then re-open this panel.`; body.appendChild(empty); return; } @@ -10568,7 +10569,7 @@ void main() { box.className = 'stale'; box.innerHTML = ` - DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the sidecar. + DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the sidecar. `; return box; } @@ -10576,7 +10577,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running ${IMPECCABLE_COMMAND} document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/.claude/skills/impeccable/scripts/live-server.mjs b/.claude/skills/impeccable/scripts/live-server.mjs index 27005ef3c..5735f2aec 100644 --- a/.claude/skills/impeccable/scripts/live-server.mjs +++ b/.claude/skills/impeccable/scripts/live-server.mjs @@ -36,6 +36,7 @@ import { getDesignSidecarPath, getLiveDir, getLiveAnnotationsDir, + IMPECCABLE_COMMAND_PREFIX, readLiveServerInfo, removeLiveServerInfo, resolveDesignSidecarPath, @@ -413,6 +414,7 @@ function createRequestHandler({ detectScript, liveScriptParts }) { token: state.token, port: state.port, vocabulary: LIVE_COMMANDS, + commandPrefix: IMPECCABLE_COMMAND_PREFIX, parts, }); res.writeHead(200, { diff --git a/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs b/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs index 9229e34f8..b77f6a542 100644 --- a/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.claude/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -32,10 +32,11 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { +export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', parts }) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + + `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; diff --git a/.claude/skills/impeccable/scripts/pin.mjs b/.claude/skills/impeccable/scripts/pin.mjs index b0eb39da0..52ea2701b 100644 --- a/.claude/skills/impeccable/scripts/pin.mjs +++ b/.claude/skills/impeccable/scripts/pin.mjs @@ -6,7 +6,7 @@ * node /pin.mjs pin * node /pin.mjs unpin * - * `pin audit` creates a lightweight /audit skill that redirects to /impeccable audit. + * `pin audit` creates a lightweight audit skill that redirects to Impeccable's audit workflow. * `unpin audit` removes that shortcut. * * The script discovers harness directories (.claude/skills, .cursor/skills, etc.) @@ -14,7 +14,7 @@ */ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs'; -import { join, resolve, dirname } from 'node:path'; +import { basename, join, resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -25,6 +25,8 @@ const HARNESS_DIRS = [ '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', ]; +const CODEX_HARNESSES = new Set(['.codex', '.agents']); + // Valid sub-command names const VALID_COMMANDS = [ 'craft', 'init', 'extract', 'document', 'shape', @@ -87,8 +89,12 @@ function loadCommandMetadata() { /** * Generate a pinned skill's SKILL.md content. */ -function generatePinnedSkill(command, metadata) { - const desc = metadata[command]?.description || `Shortcut for /impeccable ${command}.`; +function commandPrefixForSkillsDir(skillsDir) { + return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; +} + +function generatePinnedSkill(command, metadata, commandPrefix) { + const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; return `--- @@ -100,9 +106,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`/impeccable ${command}\`. +This is a pinned shortcut for \`${commandPrefix}impeccable ${command}\`. -Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } @@ -118,10 +124,11 @@ function pin(command, projectRoot) { return false; } - const content = generatePinnedSkill(command, metadata); let created = 0; for (const skillsDir of harnessDirs) { + const commandPrefix = commandPrefixForSkillsDir(skillsDir); + const content = generatePinnedSkill(command, metadata, commandPrefix); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { @@ -143,7 +150,7 @@ function pin(command, projectRoot) { if (created > 0) { console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`); - console.log(`You can now use /${command} directly.`); + console.log('Use the pinned command directly in each harness.'); } return created > 0; @@ -177,7 +184,7 @@ function unpin(command, projectRoot) { if (removed > 0) { console.log(`\nUnpinned '${command}' from ${removed} location(s).`); - console.log(`Use /impeccable ${command} to access it.`); + console.log(`Use Impeccable's '${command}' workflow directly to access it.`); } else { console.log(`No pinned '${command}' shortcut found.`); } diff --git a/.cursor/skills/impeccable/scripts/context-signals.mjs b/.cursor/skills/impeccable/scripts/context-signals.mjs index e12881b81..2fc27bea7 100644 --- a/.cursor/skills/impeccable/scripts/context-signals.mjs +++ b/.cursor/skills/impeccable/scripts/context-signals.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Context-signals gatherer for the bare `/impeccable` + * Context-signals gatherer for the bare Impeccable invocation * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.cursor/skills/impeccable/scripts/context.mjs b/.cursor/skills/impeccable/scripts/context.mjs index 4530f41d5..11f2aabe0 100644 --- a/.cursor/skills/impeccable/scripts/context.mjs +++ b/.cursor/skills/impeccable/scripts/context.mjs @@ -23,6 +23,7 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseTargetOptions } from './lib/target-args.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; @@ -902,7 +903,7 @@ async function cli() { 'or wording that clearly maps to a from-scratch build/shape flow, load ' + 'reference/init.md and write PRODUCT.md first; for any other (scoped) ' + 'command against existing code, proceed using the code as context and ' + - 'offer `/impeccable init` as a suggestion (do not block).', + `offer \`${IMPECCABLE_COMMAND} init\` as a suggestion (do not block).`, ]; parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists })); if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) { diff --git a/.cursor/skills/impeccable/scripts/critique-storage.mjs b/.cursor/skills/impeccable/scripts/critique-storage.mjs index 645628c8c..6b4d225cf 100644 --- a/.cursor/skills/impeccable/scripts/critique-storage.mjs +++ b/.cursor/skills/impeccable/scripts/critique-storage.mjs @@ -2,11 +2,11 @@ /** * Critique persistence helper. * - * Each run of /impeccable critique writes a per-target snapshot to + * Each critique run writes a per-target snapshot to * .impeccable/critique/__.md * with a small YAML frontmatter carrying the score + P0/P1 counts. * - * /impeccable polish reads the latest matching snapshot at start as its + * The polish workflow reads the latest matching snapshot at start as its * fix backlog. No other skill auto-reads critique output. * * The slug is derived mechanically from the *resolved* primary artifact diff --git a/.cursor/skills/impeccable/scripts/hook-admin.mjs b/.cursor/skills/impeccable/scripts/hook-admin.mjs index ce4ec7b2b..8b37b1f3b 100644 --- a/.cursor/skills/impeccable/scripts/hook-admin.mjs +++ b/.cursor/skills/impeccable/scripts/hook-admin.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * `/impeccable hooks ` — manage the design hook runtime + * The Impeccable hooks command manages the design hook runtime * via the `hook` key and shared detector ignores via the `detector` key in * .impeccable/config.json / .impeccable/config.local.json. * @@ -21,6 +21,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; import { getConfigPath, @@ -184,7 +185,7 @@ function writeHookConfig(cwd, hookConfig, opts = {}) { const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {}; const existingHook = stripDetectorKeys(hookSection(existing)); // Merge over the existing hook object so fields the merge helpers don't manage - // (consent, quiet, auditLog) survive a `/impeccable hooks` edit. + // (consent, quiet, auditLog) survive an Impeccable hooks edit. const next = { ...existing, hook: { ...existingHook, ...hookConfig } }; fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n'); @@ -513,9 +514,9 @@ function parseIgnoreRuleArgs(args) { function addIgnoreRule(cwd, args) { const parsed = parseIgnoreRuleArgs(args); const rule = parsed.rule; - if (!rule) throw new Error('Pass a rule id, e.g. /impeccable hooks ignore-rule side-tab'); + if (!rule) throw new Error(`Pass a rule id, e.g. ${IMPECCABLE_COMMAND} hooks ignore-rule side-tab`); if (rule === 'overused-font' && !parsed.allValues) { - throw new Error('overused-font is value-specific by default. Use /impeccable hooks ignore-value overused-font for a confirmed font, or /impeccable hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.'); + throw new Error(`overused-font is value-specific by default. Use ${IMPECCABLE_COMMAND} hooks ignore-value overused-font for a confirmed font, or ${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.`); } const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule); @@ -524,7 +525,7 @@ function addIgnoreRule(cwd, args) { } function addIgnoreFile(cwd, glob) { - if (!glob) throw new Error('Pass a glob, e.g. /impeccable hooks ignore-file "src/legacy/**"'); + if (!glob) throw new Error(`Pass a glob, e.g. ${IMPECCABLE_COMMAND} hooks ignore-file "src/legacy/**"`); const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob); writeDetectorConfig(cwd, config); @@ -569,7 +570,7 @@ function parseIgnoreValueArgs(args) { function addIgnoreValue(cwd, args) { const parsed = parseIgnoreValueArgs(args); if (!parsed.rule || !parsed.value) { - throw new Error('Pass a rule id and value, e.g. /impeccable hooks ignore-value overused-font Inter'); + throw new Error(`Pass a rule id and value, e.g. ${IMPECCABLE_COMMAND} hooks ignore-value overused-font Inter`); } if (parsed.shared && parsed.local) { diff --git a/.cursor/skills/impeccable/scripts/hook-lib.mjs b/.cursor/skills/impeccable/scripts/hook-lib.mjs index 4953b91ca..0d9722953 100644 --- a/.cursor/skills/impeccable/scripts/hook-lib.mjs +++ b/.cursor/skills/impeccable/scripts/hook-lib.mjs @@ -41,6 +41,7 @@ import os from 'node:os'; import path from 'node:path'; import { pathToFileURL, fileURLToPath } from 'node:url'; import { extractPlatform, loadContext } from './context.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -661,7 +662,7 @@ export function bumpEditCount(cache, sessionId, filePath) { } export function suppressionNotice(filePath) { - return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run /impeccable audit to revisit.`; + return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run ${IMPECCABLE_COMMAND} audit to revisit.`; } // Glob → RegExp. Supports `**`, `*`, `?`, and `{a,b}` alternation. @@ -877,7 +878,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) { const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`; const lines = shown.map((f) => formatFindingLine(f)); const more = remaining > 0 - ? `... and ${remaining} more (see /impeccable audit).` + ? `... and ${remaining} more (see ${IMPECCABLE_COMMAND} audit).` : null; const footer = directiveFooter(display); @@ -921,7 +922,7 @@ function renderGroupedTemplate(groups, config, opts = {}) { shownCount += shown.length; const hidden = group.findings.length - shown.length; if (hidden > 0) { - lines.push(`- ... ${hidden} more in ${display} (see /impeccable audit).`); + lines.push(`- ... ${hidden} more in ${display} (see ${IMPECCABLE_COMMAND} audit).`); } } @@ -937,7 +938,7 @@ function clampGroupedToBudget(header, lines, footer, maxChars) { const assemble = (linesArr, omitted) => [ header, ...linesArr, - ...(omitted ? ['... and more (see /impeccable audit).'] : []), + ...(omitted ? [`... and more (see ${IMPECCABLE_COMMAND} audit).`] : []), '', footer, ].join('\n'); @@ -970,7 +971,7 @@ function clampToBudget(header, lines, more, footer, maxChars) { let assembled = assemble(working, moreText); while (assembled.length > maxChars && working.length > 1) { working.pop(); - moreText = '... and more (see /impeccable audit).'; + moreText = `... and more (see ${IMPECCABLE_COMMAND} audit).`; assembled = assemble(working, moreText); } if (assembled.length > maxChars) { @@ -1002,7 +1003,7 @@ function formatFindingIgnoreCommand(finding) { const value = extractFindingIgnoreValueRaw(finding); const valueArg = quoteCommandArg(value); const reason = quoteCommandArg(`User confirmed ${value} is intentional`); - return `/impeccable hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; + return `${IMPECCABLE_COMMAND} hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; } function quoteCommandArg(value) { @@ -1447,7 +1448,7 @@ export function designSystemOptions(config, detector, projectCwd) { export function appendDesignSystemNote(text, scanOptions) { if (!text || !scanOptions?.designSystem?.mdNewerThanJson) return text; - return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the design-system sidecar.`; + return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the design-system sidecar.`; } // The directive footer is the part of the hook output that steers model @@ -1464,16 +1465,16 @@ export function appendDesignSystemNote(text, scanOptions) { // raw envelope. Asking the model to surface the resolution in its // reply is the cheapest way to make the feedback loop visible. function directiveFooter(display, opts = {}) { - const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`; + const ignoreFileCommand = `${IMPECCABLE_COMMAND} hooks ignore-file ${quoteCommandArg(display)}`; const fileIgnoreGuidance = opts.grouped - ? 'run `/impeccable hooks ignore-file ` for the specific file' + ? `run \`${IMPECCABLE_COMMAND} hooks ignore-file \` for the specific file` : `run \`${ignoreFileCommand}\``; return [ 'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.', '', 'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.', '', - `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`, + `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`${IMPECCABLE_COMMAND} hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`${IMPECCABLE_COMMAND} hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run ${IMPECCABLE_COMMAND} audit for the full pass.`, ].join('\n'); } diff --git a/.cursor/skills/impeccable/scripts/lib/impeccable-paths.mjs b/.cursor/skills/impeccable/scripts/lib/impeccable-paths.mjs index 91121dd59..2ccbe7b74 100644 --- a/.cursor/skills/impeccable/scripts/lib/impeccable-paths.mjs +++ b/.cursor/skills/impeccable/scripts/lib/impeccable-paths.mjs @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { resolveProjectRoot } from '../context.mjs'; +export { IMPECCABLE_COMMAND_PREFIX } from './provider.mjs'; export const IMPECCABLE_DIR = '.impeccable'; export const LIVE_DIR = 'live'; diff --git a/.cursor/skills/impeccable/scripts/lib/provider.mjs b/.cursor/skills/impeccable/scripts/lib/provider.mjs new file mode 100644 index 000000000..7fa928a9c --- /dev/null +++ b/.cursor/skills/impeccable/scripts/lib/provider.mjs @@ -0,0 +1,4 @@ +// Source scripts default to slash commands. The provider build replaces only +// this exact declaration, avoiding heuristic rewrites across executable code. +export const IMPECCABLE_COMMAND_PREFIX = "/"; +export const IMPECCABLE_COMMAND = `${IMPECCABLE_COMMAND_PREFIX}impeccable`; diff --git a/.cursor/skills/impeccable/scripts/live-browser.js b/.cursor/skills/impeccable/scripts/live-browser.js index 02b8c8bcf..616f1290a 100644 --- a/.cursor/skills/impeccable/scripts/live-browser.js +++ b/.cursor/skills/impeccable/scripts/live-browser.js @@ -57,6 +57,7 @@ const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 }; const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint const PREFIX = 'impeccable-live'; + const IMPECCABLE_COMMAND = (window.__IMPECCABLE_COMMAND_PREFIX__ || '/') + 'impeccable'; const PICK_CURSOR_STYLE_ID = PREFIX + '-pick-cursor-style'; const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000; const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({ @@ -6145,7 +6146,7 @@ switch (msg.type) { case 'connected': hasProjectContext = !!msg.hasProjectContext; - if (!hasProjectContext) showToast('No PRODUCT.md found. Variants will be brand-agnostic. Run /impeccable init to generate one.', 7000); + if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000); console.log('[impeccable] Live mode connected.'); syncAgentPollingUi(!!msg.agentPolling); startAgentStatusPoll(); @@ -10538,7 +10539,7 @@ void main() { if (designState.present === false) { const empty = document.createElement('div'); empty.className = 'empty'; - empty.innerHTML = `No DESIGN.md yetCreate one by running /impeccable document in your terminal, then re-open this panel.`; + empty.innerHTML = `No DESIGN.md yetCreate one by running ${IMPECCABLE_COMMAND} document in your terminal, then re-open this panel.`; body.appendChild(empty); return; } @@ -10568,7 +10569,7 @@ void main() { box.className = 'stale'; box.innerHTML = ` - DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the sidecar. + DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the sidecar. `; return box; } @@ -10576,7 +10577,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running ${IMPECCABLE_COMMAND} document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/.cursor/skills/impeccable/scripts/live-server.mjs b/.cursor/skills/impeccable/scripts/live-server.mjs index 27005ef3c..5735f2aec 100644 --- a/.cursor/skills/impeccable/scripts/live-server.mjs +++ b/.cursor/skills/impeccable/scripts/live-server.mjs @@ -36,6 +36,7 @@ import { getDesignSidecarPath, getLiveDir, getLiveAnnotationsDir, + IMPECCABLE_COMMAND_PREFIX, readLiveServerInfo, removeLiveServerInfo, resolveDesignSidecarPath, @@ -413,6 +414,7 @@ function createRequestHandler({ detectScript, liveScriptParts }) { token: state.token, port: state.port, vocabulary: LIVE_COMMANDS, + commandPrefix: IMPECCABLE_COMMAND_PREFIX, parts, }); res.writeHead(200, { diff --git a/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs b/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs index 9229e34f8..b77f6a542 100644 --- a/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.cursor/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -32,10 +32,11 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { +export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', parts }) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + + `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; diff --git a/.cursor/skills/impeccable/scripts/pin.mjs b/.cursor/skills/impeccable/scripts/pin.mjs index b0eb39da0..52ea2701b 100644 --- a/.cursor/skills/impeccable/scripts/pin.mjs +++ b/.cursor/skills/impeccable/scripts/pin.mjs @@ -6,7 +6,7 @@ * node /pin.mjs pin * node /pin.mjs unpin * - * `pin audit` creates a lightweight /audit skill that redirects to /impeccable audit. + * `pin audit` creates a lightweight audit skill that redirects to Impeccable's audit workflow. * `unpin audit` removes that shortcut. * * The script discovers harness directories (.claude/skills, .cursor/skills, etc.) @@ -14,7 +14,7 @@ */ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs'; -import { join, resolve, dirname } from 'node:path'; +import { basename, join, resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -25,6 +25,8 @@ const HARNESS_DIRS = [ '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', ]; +const CODEX_HARNESSES = new Set(['.codex', '.agents']); + // Valid sub-command names const VALID_COMMANDS = [ 'craft', 'init', 'extract', 'document', 'shape', @@ -87,8 +89,12 @@ function loadCommandMetadata() { /** * Generate a pinned skill's SKILL.md content. */ -function generatePinnedSkill(command, metadata) { - const desc = metadata[command]?.description || `Shortcut for /impeccable ${command}.`; +function commandPrefixForSkillsDir(skillsDir) { + return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; +} + +function generatePinnedSkill(command, metadata, commandPrefix) { + const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; return `--- @@ -100,9 +106,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`/impeccable ${command}\`. +This is a pinned shortcut for \`${commandPrefix}impeccable ${command}\`. -Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } @@ -118,10 +124,11 @@ function pin(command, projectRoot) { return false; } - const content = generatePinnedSkill(command, metadata); let created = 0; for (const skillsDir of harnessDirs) { + const commandPrefix = commandPrefixForSkillsDir(skillsDir); + const content = generatePinnedSkill(command, metadata, commandPrefix); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { @@ -143,7 +150,7 @@ function pin(command, projectRoot) { if (created > 0) { console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`); - console.log(`You can now use /${command} directly.`); + console.log('Use the pinned command directly in each harness.'); } return created > 0; @@ -177,7 +184,7 @@ function unpin(command, projectRoot) { if (removed > 0) { console.log(`\nUnpinned '${command}' from ${removed} location(s).`); - console.log(`Use /impeccable ${command} to access it.`); + console.log(`Use Impeccable's '${command}' workflow directly to access it.`); } else { console.log(`No pinned '${command}' shortcut found.`); } diff --git a/.gemini/skills/impeccable/scripts/context-signals.mjs b/.gemini/skills/impeccable/scripts/context-signals.mjs index e12881b81..2fc27bea7 100644 --- a/.gemini/skills/impeccable/scripts/context-signals.mjs +++ b/.gemini/skills/impeccable/scripts/context-signals.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Context-signals gatherer for the bare `/impeccable` + * Context-signals gatherer for the bare Impeccable invocation * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.gemini/skills/impeccable/scripts/context.mjs b/.gemini/skills/impeccable/scripts/context.mjs index 4530f41d5..11f2aabe0 100644 --- a/.gemini/skills/impeccable/scripts/context.mjs +++ b/.gemini/skills/impeccable/scripts/context.mjs @@ -23,6 +23,7 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseTargetOptions } from './lib/target-args.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; @@ -902,7 +903,7 @@ async function cli() { 'or wording that clearly maps to a from-scratch build/shape flow, load ' + 'reference/init.md and write PRODUCT.md first; for any other (scoped) ' + 'command against existing code, proceed using the code as context and ' + - 'offer `/impeccable init` as a suggestion (do not block).', + `offer \`${IMPECCABLE_COMMAND} init\` as a suggestion (do not block).`, ]; parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists })); if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) { diff --git a/.gemini/skills/impeccable/scripts/critique-storage.mjs b/.gemini/skills/impeccable/scripts/critique-storage.mjs index 645628c8c..6b4d225cf 100644 --- a/.gemini/skills/impeccable/scripts/critique-storage.mjs +++ b/.gemini/skills/impeccable/scripts/critique-storage.mjs @@ -2,11 +2,11 @@ /** * Critique persistence helper. * - * Each run of /impeccable critique writes a per-target snapshot to + * Each critique run writes a per-target snapshot to * .impeccable/critique/__.md * with a small YAML frontmatter carrying the score + P0/P1 counts. * - * /impeccable polish reads the latest matching snapshot at start as its + * The polish workflow reads the latest matching snapshot at start as its * fix backlog. No other skill auto-reads critique output. * * The slug is derived mechanically from the *resolved* primary artifact diff --git a/.gemini/skills/impeccable/scripts/hook-admin.mjs b/.gemini/skills/impeccable/scripts/hook-admin.mjs index ce4ec7b2b..8b37b1f3b 100644 --- a/.gemini/skills/impeccable/scripts/hook-admin.mjs +++ b/.gemini/skills/impeccable/scripts/hook-admin.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * `/impeccable hooks ` — manage the design hook runtime + * The Impeccable hooks command manages the design hook runtime * via the `hook` key and shared detector ignores via the `detector` key in * .impeccable/config.json / .impeccable/config.local.json. * @@ -21,6 +21,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; import { getConfigPath, @@ -184,7 +185,7 @@ function writeHookConfig(cwd, hookConfig, opts = {}) { const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {}; const existingHook = stripDetectorKeys(hookSection(existing)); // Merge over the existing hook object so fields the merge helpers don't manage - // (consent, quiet, auditLog) survive a `/impeccable hooks` edit. + // (consent, quiet, auditLog) survive an Impeccable hooks edit. const next = { ...existing, hook: { ...existingHook, ...hookConfig } }; fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n'); @@ -513,9 +514,9 @@ function parseIgnoreRuleArgs(args) { function addIgnoreRule(cwd, args) { const parsed = parseIgnoreRuleArgs(args); const rule = parsed.rule; - if (!rule) throw new Error('Pass a rule id, e.g. /impeccable hooks ignore-rule side-tab'); + if (!rule) throw new Error(`Pass a rule id, e.g. ${IMPECCABLE_COMMAND} hooks ignore-rule side-tab`); if (rule === 'overused-font' && !parsed.allValues) { - throw new Error('overused-font is value-specific by default. Use /impeccable hooks ignore-value overused-font for a confirmed font, or /impeccable hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.'); + throw new Error(`overused-font is value-specific by default. Use ${IMPECCABLE_COMMAND} hooks ignore-value overused-font for a confirmed font, or ${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.`); } const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule); @@ -524,7 +525,7 @@ function addIgnoreRule(cwd, args) { } function addIgnoreFile(cwd, glob) { - if (!glob) throw new Error('Pass a glob, e.g. /impeccable hooks ignore-file "src/legacy/**"'); + if (!glob) throw new Error(`Pass a glob, e.g. ${IMPECCABLE_COMMAND} hooks ignore-file "src/legacy/**"`); const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob); writeDetectorConfig(cwd, config); @@ -569,7 +570,7 @@ function parseIgnoreValueArgs(args) { function addIgnoreValue(cwd, args) { const parsed = parseIgnoreValueArgs(args); if (!parsed.rule || !parsed.value) { - throw new Error('Pass a rule id and value, e.g. /impeccable hooks ignore-value overused-font Inter'); + throw new Error(`Pass a rule id and value, e.g. ${IMPECCABLE_COMMAND} hooks ignore-value overused-font Inter`); } if (parsed.shared && parsed.local) { diff --git a/.gemini/skills/impeccable/scripts/hook-lib.mjs b/.gemini/skills/impeccable/scripts/hook-lib.mjs index 4953b91ca..0d9722953 100644 --- a/.gemini/skills/impeccable/scripts/hook-lib.mjs +++ b/.gemini/skills/impeccable/scripts/hook-lib.mjs @@ -41,6 +41,7 @@ import os from 'node:os'; import path from 'node:path'; import { pathToFileURL, fileURLToPath } from 'node:url'; import { extractPlatform, loadContext } from './context.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -661,7 +662,7 @@ export function bumpEditCount(cache, sessionId, filePath) { } export function suppressionNotice(filePath) { - return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run /impeccable audit to revisit.`; + return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run ${IMPECCABLE_COMMAND} audit to revisit.`; } // Glob → RegExp. Supports `**`, `*`, `?`, and `{a,b}` alternation. @@ -877,7 +878,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) { const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`; const lines = shown.map((f) => formatFindingLine(f)); const more = remaining > 0 - ? `... and ${remaining} more (see /impeccable audit).` + ? `... and ${remaining} more (see ${IMPECCABLE_COMMAND} audit).` : null; const footer = directiveFooter(display); @@ -921,7 +922,7 @@ function renderGroupedTemplate(groups, config, opts = {}) { shownCount += shown.length; const hidden = group.findings.length - shown.length; if (hidden > 0) { - lines.push(`- ... ${hidden} more in ${display} (see /impeccable audit).`); + lines.push(`- ... ${hidden} more in ${display} (see ${IMPECCABLE_COMMAND} audit).`); } } @@ -937,7 +938,7 @@ function clampGroupedToBudget(header, lines, footer, maxChars) { const assemble = (linesArr, omitted) => [ header, ...linesArr, - ...(omitted ? ['... and more (see /impeccable audit).'] : []), + ...(omitted ? [`... and more (see ${IMPECCABLE_COMMAND} audit).`] : []), '', footer, ].join('\n'); @@ -970,7 +971,7 @@ function clampToBudget(header, lines, more, footer, maxChars) { let assembled = assemble(working, moreText); while (assembled.length > maxChars && working.length > 1) { working.pop(); - moreText = '... and more (see /impeccable audit).'; + moreText = `... and more (see ${IMPECCABLE_COMMAND} audit).`; assembled = assemble(working, moreText); } if (assembled.length > maxChars) { @@ -1002,7 +1003,7 @@ function formatFindingIgnoreCommand(finding) { const value = extractFindingIgnoreValueRaw(finding); const valueArg = quoteCommandArg(value); const reason = quoteCommandArg(`User confirmed ${value} is intentional`); - return `/impeccable hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; + return `${IMPECCABLE_COMMAND} hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; } function quoteCommandArg(value) { @@ -1447,7 +1448,7 @@ export function designSystemOptions(config, detector, projectCwd) { export function appendDesignSystemNote(text, scanOptions) { if (!text || !scanOptions?.designSystem?.mdNewerThanJson) return text; - return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the design-system sidecar.`; + return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the design-system sidecar.`; } // The directive footer is the part of the hook output that steers model @@ -1464,16 +1465,16 @@ export function appendDesignSystemNote(text, scanOptions) { // raw envelope. Asking the model to surface the resolution in its // reply is the cheapest way to make the feedback loop visible. function directiveFooter(display, opts = {}) { - const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`; + const ignoreFileCommand = `${IMPECCABLE_COMMAND} hooks ignore-file ${quoteCommandArg(display)}`; const fileIgnoreGuidance = opts.grouped - ? 'run `/impeccable hooks ignore-file ` for the specific file' + ? `run \`${IMPECCABLE_COMMAND} hooks ignore-file \` for the specific file` : `run \`${ignoreFileCommand}\``; return [ 'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.', '', 'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.', '', - `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`, + `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`${IMPECCABLE_COMMAND} hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`${IMPECCABLE_COMMAND} hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run ${IMPECCABLE_COMMAND} audit for the full pass.`, ].join('\n'); } diff --git a/.gemini/skills/impeccable/scripts/lib/impeccable-paths.mjs b/.gemini/skills/impeccable/scripts/lib/impeccable-paths.mjs index 91121dd59..2ccbe7b74 100644 --- a/.gemini/skills/impeccable/scripts/lib/impeccable-paths.mjs +++ b/.gemini/skills/impeccable/scripts/lib/impeccable-paths.mjs @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { resolveProjectRoot } from '../context.mjs'; +export { IMPECCABLE_COMMAND_PREFIX } from './provider.mjs'; export const IMPECCABLE_DIR = '.impeccable'; export const LIVE_DIR = 'live'; diff --git a/.gemini/skills/impeccable/scripts/lib/provider.mjs b/.gemini/skills/impeccable/scripts/lib/provider.mjs new file mode 100644 index 000000000..7fa928a9c --- /dev/null +++ b/.gemini/skills/impeccable/scripts/lib/provider.mjs @@ -0,0 +1,4 @@ +// Source scripts default to slash commands. The provider build replaces only +// this exact declaration, avoiding heuristic rewrites across executable code. +export const IMPECCABLE_COMMAND_PREFIX = "/"; +export const IMPECCABLE_COMMAND = `${IMPECCABLE_COMMAND_PREFIX}impeccable`; diff --git a/.gemini/skills/impeccable/scripts/live-browser.js b/.gemini/skills/impeccable/scripts/live-browser.js index 02b8c8bcf..616f1290a 100644 --- a/.gemini/skills/impeccable/scripts/live-browser.js +++ b/.gemini/skills/impeccable/scripts/live-browser.js @@ -57,6 +57,7 @@ const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 }; const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint const PREFIX = 'impeccable-live'; + const IMPECCABLE_COMMAND = (window.__IMPECCABLE_COMMAND_PREFIX__ || '/') + 'impeccable'; const PICK_CURSOR_STYLE_ID = PREFIX + '-pick-cursor-style'; const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000; const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({ @@ -6145,7 +6146,7 @@ switch (msg.type) { case 'connected': hasProjectContext = !!msg.hasProjectContext; - if (!hasProjectContext) showToast('No PRODUCT.md found. Variants will be brand-agnostic. Run /impeccable init to generate one.', 7000); + if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000); console.log('[impeccable] Live mode connected.'); syncAgentPollingUi(!!msg.agentPolling); startAgentStatusPoll(); @@ -10538,7 +10539,7 @@ void main() { if (designState.present === false) { const empty = document.createElement('div'); empty.className = 'empty'; - empty.innerHTML = `No DESIGN.md yetCreate one by running /impeccable document in your terminal, then re-open this panel.`; + empty.innerHTML = `No DESIGN.md yetCreate one by running ${IMPECCABLE_COMMAND} document in your terminal, then re-open this panel.`; body.appendChild(empty); return; } @@ -10568,7 +10569,7 @@ void main() { box.className = 'stale'; box.innerHTML = ` - DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the sidecar. + DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the sidecar. `; return box; } @@ -10576,7 +10577,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running ${IMPECCABLE_COMMAND} document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/.gemini/skills/impeccable/scripts/live-server.mjs b/.gemini/skills/impeccable/scripts/live-server.mjs index 27005ef3c..5735f2aec 100644 --- a/.gemini/skills/impeccable/scripts/live-server.mjs +++ b/.gemini/skills/impeccable/scripts/live-server.mjs @@ -36,6 +36,7 @@ import { getDesignSidecarPath, getLiveDir, getLiveAnnotationsDir, + IMPECCABLE_COMMAND_PREFIX, readLiveServerInfo, removeLiveServerInfo, resolveDesignSidecarPath, @@ -413,6 +414,7 @@ function createRequestHandler({ detectScript, liveScriptParts }) { token: state.token, port: state.port, vocabulary: LIVE_COMMANDS, + commandPrefix: IMPECCABLE_COMMAND_PREFIX, parts, }); res.writeHead(200, { diff --git a/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs b/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs index 9229e34f8..b77f6a542 100644 --- a/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.gemini/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -32,10 +32,11 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { +export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', parts }) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + + `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; diff --git a/.gemini/skills/impeccable/scripts/pin.mjs b/.gemini/skills/impeccable/scripts/pin.mjs index b0eb39da0..52ea2701b 100644 --- a/.gemini/skills/impeccable/scripts/pin.mjs +++ b/.gemini/skills/impeccable/scripts/pin.mjs @@ -6,7 +6,7 @@ * node /pin.mjs pin * node /pin.mjs unpin * - * `pin audit` creates a lightweight /audit skill that redirects to /impeccable audit. + * `pin audit` creates a lightweight audit skill that redirects to Impeccable's audit workflow. * `unpin audit` removes that shortcut. * * The script discovers harness directories (.claude/skills, .cursor/skills, etc.) @@ -14,7 +14,7 @@ */ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs'; -import { join, resolve, dirname } from 'node:path'; +import { basename, join, resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -25,6 +25,8 @@ const HARNESS_DIRS = [ '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', ]; +const CODEX_HARNESSES = new Set(['.codex', '.agents']); + // Valid sub-command names const VALID_COMMANDS = [ 'craft', 'init', 'extract', 'document', 'shape', @@ -87,8 +89,12 @@ function loadCommandMetadata() { /** * Generate a pinned skill's SKILL.md content. */ -function generatePinnedSkill(command, metadata) { - const desc = metadata[command]?.description || `Shortcut for /impeccable ${command}.`; +function commandPrefixForSkillsDir(skillsDir) { + return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; +} + +function generatePinnedSkill(command, metadata, commandPrefix) { + const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; return `--- @@ -100,9 +106,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`/impeccable ${command}\`. +This is a pinned shortcut for \`${commandPrefix}impeccable ${command}\`. -Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } @@ -118,10 +124,11 @@ function pin(command, projectRoot) { return false; } - const content = generatePinnedSkill(command, metadata); let created = 0; for (const skillsDir of harnessDirs) { + const commandPrefix = commandPrefixForSkillsDir(skillsDir); + const content = generatePinnedSkill(command, metadata, commandPrefix); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { @@ -143,7 +150,7 @@ function pin(command, projectRoot) { if (created > 0) { console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`); - console.log(`You can now use /${command} directly.`); + console.log('Use the pinned command directly in each harness.'); } return created > 0; @@ -177,7 +184,7 @@ function unpin(command, projectRoot) { if (removed > 0) { console.log(`\nUnpinned '${command}' from ${removed} location(s).`); - console.log(`Use /impeccable ${command} to access it.`); + console.log(`Use Impeccable's '${command}' workflow directly to access it.`); } else { console.log(`No pinned '${command}' shortcut found.`); } diff --git a/.github/skills/impeccable/scripts/context-signals.mjs b/.github/skills/impeccable/scripts/context-signals.mjs index e12881b81..2fc27bea7 100644 --- a/.github/skills/impeccable/scripts/context-signals.mjs +++ b/.github/skills/impeccable/scripts/context-signals.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Context-signals gatherer for the bare `/impeccable` + * Context-signals gatherer for the bare Impeccable invocation * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.github/skills/impeccable/scripts/context.mjs b/.github/skills/impeccable/scripts/context.mjs index 4530f41d5..11f2aabe0 100644 --- a/.github/skills/impeccable/scripts/context.mjs +++ b/.github/skills/impeccable/scripts/context.mjs @@ -23,6 +23,7 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseTargetOptions } from './lib/target-args.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; @@ -902,7 +903,7 @@ async function cli() { 'or wording that clearly maps to a from-scratch build/shape flow, load ' + 'reference/init.md and write PRODUCT.md first; for any other (scoped) ' + 'command against existing code, proceed using the code as context and ' + - 'offer `/impeccable init` as a suggestion (do not block).', + `offer \`${IMPECCABLE_COMMAND} init\` as a suggestion (do not block).`, ]; parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists })); if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) { diff --git a/.github/skills/impeccable/scripts/critique-storage.mjs b/.github/skills/impeccable/scripts/critique-storage.mjs index 645628c8c..6b4d225cf 100644 --- a/.github/skills/impeccable/scripts/critique-storage.mjs +++ b/.github/skills/impeccable/scripts/critique-storage.mjs @@ -2,11 +2,11 @@ /** * Critique persistence helper. * - * Each run of /impeccable critique writes a per-target snapshot to + * Each critique run writes a per-target snapshot to * .impeccable/critique/__.md * with a small YAML frontmatter carrying the score + P0/P1 counts. * - * /impeccable polish reads the latest matching snapshot at start as its + * The polish workflow reads the latest matching snapshot at start as its * fix backlog. No other skill auto-reads critique output. * * The slug is derived mechanically from the *resolved* primary artifact diff --git a/.github/skills/impeccable/scripts/hook-admin.mjs b/.github/skills/impeccable/scripts/hook-admin.mjs index ce4ec7b2b..8b37b1f3b 100644 --- a/.github/skills/impeccable/scripts/hook-admin.mjs +++ b/.github/skills/impeccable/scripts/hook-admin.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * `/impeccable hooks ` — manage the design hook runtime + * The Impeccable hooks command manages the design hook runtime * via the `hook` key and shared detector ignores via the `detector` key in * .impeccable/config.json / .impeccable/config.local.json. * @@ -21,6 +21,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; import { getConfigPath, @@ -184,7 +185,7 @@ function writeHookConfig(cwd, hookConfig, opts = {}) { const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {}; const existingHook = stripDetectorKeys(hookSection(existing)); // Merge over the existing hook object so fields the merge helpers don't manage - // (consent, quiet, auditLog) survive a `/impeccable hooks` edit. + // (consent, quiet, auditLog) survive an Impeccable hooks edit. const next = { ...existing, hook: { ...existingHook, ...hookConfig } }; fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n'); @@ -513,9 +514,9 @@ function parseIgnoreRuleArgs(args) { function addIgnoreRule(cwd, args) { const parsed = parseIgnoreRuleArgs(args); const rule = parsed.rule; - if (!rule) throw new Error('Pass a rule id, e.g. /impeccable hooks ignore-rule side-tab'); + if (!rule) throw new Error(`Pass a rule id, e.g. ${IMPECCABLE_COMMAND} hooks ignore-rule side-tab`); if (rule === 'overused-font' && !parsed.allValues) { - throw new Error('overused-font is value-specific by default. Use /impeccable hooks ignore-value overused-font for a confirmed font, or /impeccable hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.'); + throw new Error(`overused-font is value-specific by default. Use ${IMPECCABLE_COMMAND} hooks ignore-value overused-font for a confirmed font, or ${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.`); } const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule); @@ -524,7 +525,7 @@ function addIgnoreRule(cwd, args) { } function addIgnoreFile(cwd, glob) { - if (!glob) throw new Error('Pass a glob, e.g. /impeccable hooks ignore-file "src/legacy/**"'); + if (!glob) throw new Error(`Pass a glob, e.g. ${IMPECCABLE_COMMAND} hooks ignore-file "src/legacy/**"`); const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob); writeDetectorConfig(cwd, config); @@ -569,7 +570,7 @@ function parseIgnoreValueArgs(args) { function addIgnoreValue(cwd, args) { const parsed = parseIgnoreValueArgs(args); if (!parsed.rule || !parsed.value) { - throw new Error('Pass a rule id and value, e.g. /impeccable hooks ignore-value overused-font Inter'); + throw new Error(`Pass a rule id and value, e.g. ${IMPECCABLE_COMMAND} hooks ignore-value overused-font Inter`); } if (parsed.shared && parsed.local) { diff --git a/.github/skills/impeccable/scripts/hook-lib.mjs b/.github/skills/impeccable/scripts/hook-lib.mjs index 4953b91ca..0d9722953 100644 --- a/.github/skills/impeccable/scripts/hook-lib.mjs +++ b/.github/skills/impeccable/scripts/hook-lib.mjs @@ -41,6 +41,7 @@ import os from 'node:os'; import path from 'node:path'; import { pathToFileURL, fileURLToPath } from 'node:url'; import { extractPlatform, loadContext } from './context.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -661,7 +662,7 @@ export function bumpEditCount(cache, sessionId, filePath) { } export function suppressionNotice(filePath) { - return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run /impeccable audit to revisit.`; + return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run ${IMPECCABLE_COMMAND} audit to revisit.`; } // Glob → RegExp. Supports `**`, `*`, `?`, and `{a,b}` alternation. @@ -877,7 +878,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) { const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`; const lines = shown.map((f) => formatFindingLine(f)); const more = remaining > 0 - ? `... and ${remaining} more (see /impeccable audit).` + ? `... and ${remaining} more (see ${IMPECCABLE_COMMAND} audit).` : null; const footer = directiveFooter(display); @@ -921,7 +922,7 @@ function renderGroupedTemplate(groups, config, opts = {}) { shownCount += shown.length; const hidden = group.findings.length - shown.length; if (hidden > 0) { - lines.push(`- ... ${hidden} more in ${display} (see /impeccable audit).`); + lines.push(`- ... ${hidden} more in ${display} (see ${IMPECCABLE_COMMAND} audit).`); } } @@ -937,7 +938,7 @@ function clampGroupedToBudget(header, lines, footer, maxChars) { const assemble = (linesArr, omitted) => [ header, ...linesArr, - ...(omitted ? ['... and more (see /impeccable audit).'] : []), + ...(omitted ? [`... and more (see ${IMPECCABLE_COMMAND} audit).`] : []), '', footer, ].join('\n'); @@ -970,7 +971,7 @@ function clampToBudget(header, lines, more, footer, maxChars) { let assembled = assemble(working, moreText); while (assembled.length > maxChars && working.length > 1) { working.pop(); - moreText = '... and more (see /impeccable audit).'; + moreText = `... and more (see ${IMPECCABLE_COMMAND} audit).`; assembled = assemble(working, moreText); } if (assembled.length > maxChars) { @@ -1002,7 +1003,7 @@ function formatFindingIgnoreCommand(finding) { const value = extractFindingIgnoreValueRaw(finding); const valueArg = quoteCommandArg(value); const reason = quoteCommandArg(`User confirmed ${value} is intentional`); - return `/impeccable hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; + return `${IMPECCABLE_COMMAND} hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; } function quoteCommandArg(value) { @@ -1447,7 +1448,7 @@ export function designSystemOptions(config, detector, projectCwd) { export function appendDesignSystemNote(text, scanOptions) { if (!text || !scanOptions?.designSystem?.mdNewerThanJson) return text; - return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the design-system sidecar.`; + return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the design-system sidecar.`; } // The directive footer is the part of the hook output that steers model @@ -1464,16 +1465,16 @@ export function appendDesignSystemNote(text, scanOptions) { // raw envelope. Asking the model to surface the resolution in its // reply is the cheapest way to make the feedback loop visible. function directiveFooter(display, opts = {}) { - const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`; + const ignoreFileCommand = `${IMPECCABLE_COMMAND} hooks ignore-file ${quoteCommandArg(display)}`; const fileIgnoreGuidance = opts.grouped - ? 'run `/impeccable hooks ignore-file ` for the specific file' + ? `run \`${IMPECCABLE_COMMAND} hooks ignore-file \` for the specific file` : `run \`${ignoreFileCommand}\``; return [ 'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.', '', 'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.', '', - `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`, + `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`${IMPECCABLE_COMMAND} hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`${IMPECCABLE_COMMAND} hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run ${IMPECCABLE_COMMAND} audit for the full pass.`, ].join('\n'); } diff --git a/.github/skills/impeccable/scripts/lib/impeccable-paths.mjs b/.github/skills/impeccable/scripts/lib/impeccable-paths.mjs index 91121dd59..2ccbe7b74 100644 --- a/.github/skills/impeccable/scripts/lib/impeccable-paths.mjs +++ b/.github/skills/impeccable/scripts/lib/impeccable-paths.mjs @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { resolveProjectRoot } from '../context.mjs'; +export { IMPECCABLE_COMMAND_PREFIX } from './provider.mjs'; export const IMPECCABLE_DIR = '.impeccable'; export const LIVE_DIR = 'live'; diff --git a/.github/skills/impeccable/scripts/lib/provider.mjs b/.github/skills/impeccable/scripts/lib/provider.mjs new file mode 100644 index 000000000..7fa928a9c --- /dev/null +++ b/.github/skills/impeccable/scripts/lib/provider.mjs @@ -0,0 +1,4 @@ +// Source scripts default to slash commands. The provider build replaces only +// this exact declaration, avoiding heuristic rewrites across executable code. +export const IMPECCABLE_COMMAND_PREFIX = "/"; +export const IMPECCABLE_COMMAND = `${IMPECCABLE_COMMAND_PREFIX}impeccable`; diff --git a/.github/skills/impeccable/scripts/live-browser.js b/.github/skills/impeccable/scripts/live-browser.js index 02b8c8bcf..616f1290a 100644 --- a/.github/skills/impeccable/scripts/live-browser.js +++ b/.github/skills/impeccable/scripts/live-browser.js @@ -57,6 +57,7 @@ const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 }; const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint const PREFIX = 'impeccable-live'; + const IMPECCABLE_COMMAND = (window.__IMPECCABLE_COMMAND_PREFIX__ || '/') + 'impeccable'; const PICK_CURSOR_STYLE_ID = PREFIX + '-pick-cursor-style'; const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000; const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({ @@ -6145,7 +6146,7 @@ switch (msg.type) { case 'connected': hasProjectContext = !!msg.hasProjectContext; - if (!hasProjectContext) showToast('No PRODUCT.md found. Variants will be brand-agnostic. Run /impeccable init to generate one.', 7000); + if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000); console.log('[impeccable] Live mode connected.'); syncAgentPollingUi(!!msg.agentPolling); startAgentStatusPoll(); @@ -10538,7 +10539,7 @@ void main() { if (designState.present === false) { const empty = document.createElement('div'); empty.className = 'empty'; - empty.innerHTML = `No DESIGN.md yetCreate one by running /impeccable document in your terminal, then re-open this panel.`; + empty.innerHTML = `No DESIGN.md yetCreate one by running ${IMPECCABLE_COMMAND} document in your terminal, then re-open this panel.`; body.appendChild(empty); return; } @@ -10568,7 +10569,7 @@ void main() { box.className = 'stale'; box.innerHTML = ` - DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the sidecar. + DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the sidecar. `; return box; } @@ -10576,7 +10577,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running ${IMPECCABLE_COMMAND} document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/.github/skills/impeccable/scripts/live-server.mjs b/.github/skills/impeccable/scripts/live-server.mjs index 27005ef3c..5735f2aec 100644 --- a/.github/skills/impeccable/scripts/live-server.mjs +++ b/.github/skills/impeccable/scripts/live-server.mjs @@ -36,6 +36,7 @@ import { getDesignSidecarPath, getLiveDir, getLiveAnnotationsDir, + IMPECCABLE_COMMAND_PREFIX, readLiveServerInfo, removeLiveServerInfo, resolveDesignSidecarPath, @@ -413,6 +414,7 @@ function createRequestHandler({ detectScript, liveScriptParts }) { token: state.token, port: state.port, vocabulary: LIVE_COMMANDS, + commandPrefix: IMPECCABLE_COMMAND_PREFIX, parts, }); res.writeHead(200, { diff --git a/.github/skills/impeccable/scripts/live/browser-script-parts.mjs b/.github/skills/impeccable/scripts/live/browser-script-parts.mjs index 9229e34f8..b77f6a542 100644 --- a/.github/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.github/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -32,10 +32,11 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { +export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', parts }) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + + `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; diff --git a/.github/skills/impeccable/scripts/pin.mjs b/.github/skills/impeccable/scripts/pin.mjs index b0eb39da0..52ea2701b 100644 --- a/.github/skills/impeccable/scripts/pin.mjs +++ b/.github/skills/impeccable/scripts/pin.mjs @@ -6,7 +6,7 @@ * node /pin.mjs pin * node /pin.mjs unpin * - * `pin audit` creates a lightweight /audit skill that redirects to /impeccable audit. + * `pin audit` creates a lightweight audit skill that redirects to Impeccable's audit workflow. * `unpin audit` removes that shortcut. * * The script discovers harness directories (.claude/skills, .cursor/skills, etc.) @@ -14,7 +14,7 @@ */ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs'; -import { join, resolve, dirname } from 'node:path'; +import { basename, join, resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -25,6 +25,8 @@ const HARNESS_DIRS = [ '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', ]; +const CODEX_HARNESSES = new Set(['.codex', '.agents']); + // Valid sub-command names const VALID_COMMANDS = [ 'craft', 'init', 'extract', 'document', 'shape', @@ -87,8 +89,12 @@ function loadCommandMetadata() { /** * Generate a pinned skill's SKILL.md content. */ -function generatePinnedSkill(command, metadata) { - const desc = metadata[command]?.description || `Shortcut for /impeccable ${command}.`; +function commandPrefixForSkillsDir(skillsDir) { + return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; +} + +function generatePinnedSkill(command, metadata, commandPrefix) { + const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; return `--- @@ -100,9 +106,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`/impeccable ${command}\`. +This is a pinned shortcut for \`${commandPrefix}impeccable ${command}\`. -Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } @@ -118,10 +124,11 @@ function pin(command, projectRoot) { return false; } - const content = generatePinnedSkill(command, metadata); let created = 0; for (const skillsDir of harnessDirs) { + const commandPrefix = commandPrefixForSkillsDir(skillsDir); + const content = generatePinnedSkill(command, metadata, commandPrefix); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { @@ -143,7 +150,7 @@ function pin(command, projectRoot) { if (created > 0) { console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`); - console.log(`You can now use /${command} directly.`); + console.log('Use the pinned command directly in each harness.'); } return created > 0; @@ -177,7 +184,7 @@ function unpin(command, projectRoot) { if (removed > 0) { console.log(`\nUnpinned '${command}' from ${removed} location(s).`); - console.log(`Use /impeccable ${command} to access it.`); + console.log(`Use Impeccable's '${command}' workflow directly to access it.`); } else { console.log(`No pinned '${command}' shortcut found.`); } diff --git a/.kiro/skills/impeccable/scripts/context-signals.mjs b/.kiro/skills/impeccable/scripts/context-signals.mjs index e12881b81..2fc27bea7 100644 --- a/.kiro/skills/impeccable/scripts/context-signals.mjs +++ b/.kiro/skills/impeccable/scripts/context-signals.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Context-signals gatherer for the bare `/impeccable` + * Context-signals gatherer for the bare Impeccable invocation * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.kiro/skills/impeccable/scripts/context.mjs b/.kiro/skills/impeccable/scripts/context.mjs index 4530f41d5..11f2aabe0 100644 --- a/.kiro/skills/impeccable/scripts/context.mjs +++ b/.kiro/skills/impeccable/scripts/context.mjs @@ -23,6 +23,7 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseTargetOptions } from './lib/target-args.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; @@ -902,7 +903,7 @@ async function cli() { 'or wording that clearly maps to a from-scratch build/shape flow, load ' + 'reference/init.md and write PRODUCT.md first; for any other (scoped) ' + 'command against existing code, proceed using the code as context and ' + - 'offer `/impeccable init` as a suggestion (do not block).', + `offer \`${IMPECCABLE_COMMAND} init\` as a suggestion (do not block).`, ]; parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists })); if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) { diff --git a/.kiro/skills/impeccable/scripts/critique-storage.mjs b/.kiro/skills/impeccable/scripts/critique-storage.mjs index 645628c8c..6b4d225cf 100644 --- a/.kiro/skills/impeccable/scripts/critique-storage.mjs +++ b/.kiro/skills/impeccable/scripts/critique-storage.mjs @@ -2,11 +2,11 @@ /** * Critique persistence helper. * - * Each run of /impeccable critique writes a per-target snapshot to + * Each critique run writes a per-target snapshot to * .impeccable/critique/__.md * with a small YAML frontmatter carrying the score + P0/P1 counts. * - * /impeccable polish reads the latest matching snapshot at start as its + * The polish workflow reads the latest matching snapshot at start as its * fix backlog. No other skill auto-reads critique output. * * The slug is derived mechanically from the *resolved* primary artifact diff --git a/.kiro/skills/impeccable/scripts/hook-admin.mjs b/.kiro/skills/impeccable/scripts/hook-admin.mjs index ce4ec7b2b..8b37b1f3b 100644 --- a/.kiro/skills/impeccable/scripts/hook-admin.mjs +++ b/.kiro/skills/impeccable/scripts/hook-admin.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * `/impeccable hooks ` — manage the design hook runtime + * The Impeccable hooks command manages the design hook runtime * via the `hook` key and shared detector ignores via the `detector` key in * .impeccable/config.json / .impeccable/config.local.json. * @@ -21,6 +21,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; import { getConfigPath, @@ -184,7 +185,7 @@ function writeHookConfig(cwd, hookConfig, opts = {}) { const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {}; const existingHook = stripDetectorKeys(hookSection(existing)); // Merge over the existing hook object so fields the merge helpers don't manage - // (consent, quiet, auditLog) survive a `/impeccable hooks` edit. + // (consent, quiet, auditLog) survive an Impeccable hooks edit. const next = { ...existing, hook: { ...existingHook, ...hookConfig } }; fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n'); @@ -513,9 +514,9 @@ function parseIgnoreRuleArgs(args) { function addIgnoreRule(cwd, args) { const parsed = parseIgnoreRuleArgs(args); const rule = parsed.rule; - if (!rule) throw new Error('Pass a rule id, e.g. /impeccable hooks ignore-rule side-tab'); + if (!rule) throw new Error(`Pass a rule id, e.g. ${IMPECCABLE_COMMAND} hooks ignore-rule side-tab`); if (rule === 'overused-font' && !parsed.allValues) { - throw new Error('overused-font is value-specific by default. Use /impeccable hooks ignore-value overused-font for a confirmed font, or /impeccable hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.'); + throw new Error(`overused-font is value-specific by default. Use ${IMPECCABLE_COMMAND} hooks ignore-value overused-font for a confirmed font, or ${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.`); } const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule); @@ -524,7 +525,7 @@ function addIgnoreRule(cwd, args) { } function addIgnoreFile(cwd, glob) { - if (!glob) throw new Error('Pass a glob, e.g. /impeccable hooks ignore-file "src/legacy/**"'); + if (!glob) throw new Error(`Pass a glob, e.g. ${IMPECCABLE_COMMAND} hooks ignore-file "src/legacy/**"`); const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob); writeDetectorConfig(cwd, config); @@ -569,7 +570,7 @@ function parseIgnoreValueArgs(args) { function addIgnoreValue(cwd, args) { const parsed = parseIgnoreValueArgs(args); if (!parsed.rule || !parsed.value) { - throw new Error('Pass a rule id and value, e.g. /impeccable hooks ignore-value overused-font Inter'); + throw new Error(`Pass a rule id and value, e.g. ${IMPECCABLE_COMMAND} hooks ignore-value overused-font Inter`); } if (parsed.shared && parsed.local) { diff --git a/.kiro/skills/impeccable/scripts/hook-lib.mjs b/.kiro/skills/impeccable/scripts/hook-lib.mjs index 4953b91ca..0d9722953 100644 --- a/.kiro/skills/impeccable/scripts/hook-lib.mjs +++ b/.kiro/skills/impeccable/scripts/hook-lib.mjs @@ -41,6 +41,7 @@ import os from 'node:os'; import path from 'node:path'; import { pathToFileURL, fileURLToPath } from 'node:url'; import { extractPlatform, loadContext } from './context.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -661,7 +662,7 @@ export function bumpEditCount(cache, sessionId, filePath) { } export function suppressionNotice(filePath) { - return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run /impeccable audit to revisit.`; + return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run ${IMPECCABLE_COMMAND} audit to revisit.`; } // Glob → RegExp. Supports `**`, `*`, `?`, and `{a,b}` alternation. @@ -877,7 +878,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) { const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`; const lines = shown.map((f) => formatFindingLine(f)); const more = remaining > 0 - ? `... and ${remaining} more (see /impeccable audit).` + ? `... and ${remaining} more (see ${IMPECCABLE_COMMAND} audit).` : null; const footer = directiveFooter(display); @@ -921,7 +922,7 @@ function renderGroupedTemplate(groups, config, opts = {}) { shownCount += shown.length; const hidden = group.findings.length - shown.length; if (hidden > 0) { - lines.push(`- ... ${hidden} more in ${display} (see /impeccable audit).`); + lines.push(`- ... ${hidden} more in ${display} (see ${IMPECCABLE_COMMAND} audit).`); } } @@ -937,7 +938,7 @@ function clampGroupedToBudget(header, lines, footer, maxChars) { const assemble = (linesArr, omitted) => [ header, ...linesArr, - ...(omitted ? ['... and more (see /impeccable audit).'] : []), + ...(omitted ? [`... and more (see ${IMPECCABLE_COMMAND} audit).`] : []), '', footer, ].join('\n'); @@ -970,7 +971,7 @@ function clampToBudget(header, lines, more, footer, maxChars) { let assembled = assemble(working, moreText); while (assembled.length > maxChars && working.length > 1) { working.pop(); - moreText = '... and more (see /impeccable audit).'; + moreText = `... and more (see ${IMPECCABLE_COMMAND} audit).`; assembled = assemble(working, moreText); } if (assembled.length > maxChars) { @@ -1002,7 +1003,7 @@ function formatFindingIgnoreCommand(finding) { const value = extractFindingIgnoreValueRaw(finding); const valueArg = quoteCommandArg(value); const reason = quoteCommandArg(`User confirmed ${value} is intentional`); - return `/impeccable hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; + return `${IMPECCABLE_COMMAND} hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; } function quoteCommandArg(value) { @@ -1447,7 +1448,7 @@ export function designSystemOptions(config, detector, projectCwd) { export function appendDesignSystemNote(text, scanOptions) { if (!text || !scanOptions?.designSystem?.mdNewerThanJson) return text; - return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the design-system sidecar.`; + return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the design-system sidecar.`; } // The directive footer is the part of the hook output that steers model @@ -1464,16 +1465,16 @@ export function appendDesignSystemNote(text, scanOptions) { // raw envelope. Asking the model to surface the resolution in its // reply is the cheapest way to make the feedback loop visible. function directiveFooter(display, opts = {}) { - const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`; + const ignoreFileCommand = `${IMPECCABLE_COMMAND} hooks ignore-file ${quoteCommandArg(display)}`; const fileIgnoreGuidance = opts.grouped - ? 'run `/impeccable hooks ignore-file ` for the specific file' + ? `run \`${IMPECCABLE_COMMAND} hooks ignore-file \` for the specific file` : `run \`${ignoreFileCommand}\``; return [ 'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.', '', 'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.', '', - `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`, + `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`${IMPECCABLE_COMMAND} hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`${IMPECCABLE_COMMAND} hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run ${IMPECCABLE_COMMAND} audit for the full pass.`, ].join('\n'); } diff --git a/.kiro/skills/impeccable/scripts/lib/impeccable-paths.mjs b/.kiro/skills/impeccable/scripts/lib/impeccable-paths.mjs index 91121dd59..2ccbe7b74 100644 --- a/.kiro/skills/impeccable/scripts/lib/impeccable-paths.mjs +++ b/.kiro/skills/impeccable/scripts/lib/impeccable-paths.mjs @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { resolveProjectRoot } from '../context.mjs'; +export { IMPECCABLE_COMMAND_PREFIX } from './provider.mjs'; export const IMPECCABLE_DIR = '.impeccable'; export const LIVE_DIR = 'live'; diff --git a/.kiro/skills/impeccable/scripts/lib/provider.mjs b/.kiro/skills/impeccable/scripts/lib/provider.mjs new file mode 100644 index 000000000..7fa928a9c --- /dev/null +++ b/.kiro/skills/impeccable/scripts/lib/provider.mjs @@ -0,0 +1,4 @@ +// Source scripts default to slash commands. The provider build replaces only +// this exact declaration, avoiding heuristic rewrites across executable code. +export const IMPECCABLE_COMMAND_PREFIX = "/"; +export const IMPECCABLE_COMMAND = `${IMPECCABLE_COMMAND_PREFIX}impeccable`; diff --git a/.kiro/skills/impeccable/scripts/live-browser.js b/.kiro/skills/impeccable/scripts/live-browser.js index 02b8c8bcf..616f1290a 100644 --- a/.kiro/skills/impeccable/scripts/live-browser.js +++ b/.kiro/skills/impeccable/scripts/live-browser.js @@ -57,6 +57,7 @@ const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 }; const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint const PREFIX = 'impeccable-live'; + const IMPECCABLE_COMMAND = (window.__IMPECCABLE_COMMAND_PREFIX__ || '/') + 'impeccable'; const PICK_CURSOR_STYLE_ID = PREFIX + '-pick-cursor-style'; const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000; const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({ @@ -6145,7 +6146,7 @@ switch (msg.type) { case 'connected': hasProjectContext = !!msg.hasProjectContext; - if (!hasProjectContext) showToast('No PRODUCT.md found. Variants will be brand-agnostic. Run /impeccable init to generate one.', 7000); + if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000); console.log('[impeccable] Live mode connected.'); syncAgentPollingUi(!!msg.agentPolling); startAgentStatusPoll(); @@ -10538,7 +10539,7 @@ void main() { if (designState.present === false) { const empty = document.createElement('div'); empty.className = 'empty'; - empty.innerHTML = `No DESIGN.md yetCreate one by running /impeccable document in your terminal, then re-open this panel.`; + empty.innerHTML = `No DESIGN.md yetCreate one by running ${IMPECCABLE_COMMAND} document in your terminal, then re-open this panel.`; body.appendChild(empty); return; } @@ -10568,7 +10569,7 @@ void main() { box.className = 'stale'; box.innerHTML = ` - DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the sidecar. + DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the sidecar. `; return box; } @@ -10576,7 +10577,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running ${IMPECCABLE_COMMAND} document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/.kiro/skills/impeccable/scripts/live-server.mjs b/.kiro/skills/impeccable/scripts/live-server.mjs index 27005ef3c..5735f2aec 100644 --- a/.kiro/skills/impeccable/scripts/live-server.mjs +++ b/.kiro/skills/impeccable/scripts/live-server.mjs @@ -36,6 +36,7 @@ import { getDesignSidecarPath, getLiveDir, getLiveAnnotationsDir, + IMPECCABLE_COMMAND_PREFIX, readLiveServerInfo, removeLiveServerInfo, resolveDesignSidecarPath, @@ -413,6 +414,7 @@ function createRequestHandler({ detectScript, liveScriptParts }) { token: state.token, port: state.port, vocabulary: LIVE_COMMANDS, + commandPrefix: IMPECCABLE_COMMAND_PREFIX, parts, }); res.writeHead(200, { diff --git a/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs b/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs index 9229e34f8..b77f6a542 100644 --- a/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.kiro/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -32,10 +32,11 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { +export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', parts }) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + + `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; diff --git a/.kiro/skills/impeccable/scripts/pin.mjs b/.kiro/skills/impeccable/scripts/pin.mjs index b0eb39da0..52ea2701b 100644 --- a/.kiro/skills/impeccable/scripts/pin.mjs +++ b/.kiro/skills/impeccable/scripts/pin.mjs @@ -6,7 +6,7 @@ * node /pin.mjs pin * node /pin.mjs unpin * - * `pin audit` creates a lightweight /audit skill that redirects to /impeccable audit. + * `pin audit` creates a lightweight audit skill that redirects to Impeccable's audit workflow. * `unpin audit` removes that shortcut. * * The script discovers harness directories (.claude/skills, .cursor/skills, etc.) @@ -14,7 +14,7 @@ */ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs'; -import { join, resolve, dirname } from 'node:path'; +import { basename, join, resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -25,6 +25,8 @@ const HARNESS_DIRS = [ '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', ]; +const CODEX_HARNESSES = new Set(['.codex', '.agents']); + // Valid sub-command names const VALID_COMMANDS = [ 'craft', 'init', 'extract', 'document', 'shape', @@ -87,8 +89,12 @@ function loadCommandMetadata() { /** * Generate a pinned skill's SKILL.md content. */ -function generatePinnedSkill(command, metadata) { - const desc = metadata[command]?.description || `Shortcut for /impeccable ${command}.`; +function commandPrefixForSkillsDir(skillsDir) { + return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; +} + +function generatePinnedSkill(command, metadata, commandPrefix) { + const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; return `--- @@ -100,9 +106,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`/impeccable ${command}\`. +This is a pinned shortcut for \`${commandPrefix}impeccable ${command}\`. -Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } @@ -118,10 +124,11 @@ function pin(command, projectRoot) { return false; } - const content = generatePinnedSkill(command, metadata); let created = 0; for (const skillsDir of harnessDirs) { + const commandPrefix = commandPrefixForSkillsDir(skillsDir); + const content = generatePinnedSkill(command, metadata, commandPrefix); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { @@ -143,7 +150,7 @@ function pin(command, projectRoot) { if (created > 0) { console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`); - console.log(`You can now use /${command} directly.`); + console.log('Use the pinned command directly in each harness.'); } return created > 0; @@ -177,7 +184,7 @@ function unpin(command, projectRoot) { if (removed > 0) { console.log(`\nUnpinned '${command}' from ${removed} location(s).`); - console.log(`Use /impeccable ${command} to access it.`); + console.log(`Use Impeccable's '${command}' workflow directly to access it.`); } else { console.log(`No pinned '${command}' shortcut found.`); } diff --git a/.opencode/skills/impeccable/scripts/context-signals.mjs b/.opencode/skills/impeccable/scripts/context-signals.mjs index e12881b81..2fc27bea7 100644 --- a/.opencode/skills/impeccable/scripts/context-signals.mjs +++ b/.opencode/skills/impeccable/scripts/context-signals.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Context-signals gatherer for the bare `/impeccable` + * Context-signals gatherer for the bare Impeccable invocation * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.opencode/skills/impeccable/scripts/context.mjs b/.opencode/skills/impeccable/scripts/context.mjs index 4530f41d5..11f2aabe0 100644 --- a/.opencode/skills/impeccable/scripts/context.mjs +++ b/.opencode/skills/impeccable/scripts/context.mjs @@ -23,6 +23,7 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseTargetOptions } from './lib/target-args.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; @@ -902,7 +903,7 @@ async function cli() { 'or wording that clearly maps to a from-scratch build/shape flow, load ' + 'reference/init.md and write PRODUCT.md first; for any other (scoped) ' + 'command against existing code, proceed using the code as context and ' + - 'offer `/impeccable init` as a suggestion (do not block).', + `offer \`${IMPECCABLE_COMMAND} init\` as a suggestion (do not block).`, ]; parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists })); if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) { diff --git a/.opencode/skills/impeccable/scripts/critique-storage.mjs b/.opencode/skills/impeccable/scripts/critique-storage.mjs index 645628c8c..6b4d225cf 100644 --- a/.opencode/skills/impeccable/scripts/critique-storage.mjs +++ b/.opencode/skills/impeccable/scripts/critique-storage.mjs @@ -2,11 +2,11 @@ /** * Critique persistence helper. * - * Each run of /impeccable critique writes a per-target snapshot to + * Each critique run writes a per-target snapshot to * .impeccable/critique/__.md * with a small YAML frontmatter carrying the score + P0/P1 counts. * - * /impeccable polish reads the latest matching snapshot at start as its + * The polish workflow reads the latest matching snapshot at start as its * fix backlog. No other skill auto-reads critique output. * * The slug is derived mechanically from the *resolved* primary artifact diff --git a/.opencode/skills/impeccable/scripts/hook-admin.mjs b/.opencode/skills/impeccable/scripts/hook-admin.mjs index ce4ec7b2b..8b37b1f3b 100644 --- a/.opencode/skills/impeccable/scripts/hook-admin.mjs +++ b/.opencode/skills/impeccable/scripts/hook-admin.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * `/impeccable hooks ` — manage the design hook runtime + * The Impeccable hooks command manages the design hook runtime * via the `hook` key and shared detector ignores via the `detector` key in * .impeccable/config.json / .impeccable/config.local.json. * @@ -21,6 +21,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; import { getConfigPath, @@ -184,7 +185,7 @@ function writeHookConfig(cwd, hookConfig, opts = {}) { const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {}; const existingHook = stripDetectorKeys(hookSection(existing)); // Merge over the existing hook object so fields the merge helpers don't manage - // (consent, quiet, auditLog) survive a `/impeccable hooks` edit. + // (consent, quiet, auditLog) survive an Impeccable hooks edit. const next = { ...existing, hook: { ...existingHook, ...hookConfig } }; fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n'); @@ -513,9 +514,9 @@ function parseIgnoreRuleArgs(args) { function addIgnoreRule(cwd, args) { const parsed = parseIgnoreRuleArgs(args); const rule = parsed.rule; - if (!rule) throw new Error('Pass a rule id, e.g. /impeccable hooks ignore-rule side-tab'); + if (!rule) throw new Error(`Pass a rule id, e.g. ${IMPECCABLE_COMMAND} hooks ignore-rule side-tab`); if (rule === 'overused-font' && !parsed.allValues) { - throw new Error('overused-font is value-specific by default. Use /impeccable hooks ignore-value overused-font for a confirmed font, or /impeccable hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.'); + throw new Error(`overused-font is value-specific by default. Use ${IMPECCABLE_COMMAND} hooks ignore-value overused-font for a confirmed font, or ${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.`); } const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule); @@ -524,7 +525,7 @@ function addIgnoreRule(cwd, args) { } function addIgnoreFile(cwd, glob) { - if (!glob) throw new Error('Pass a glob, e.g. /impeccable hooks ignore-file "src/legacy/**"'); + if (!glob) throw new Error(`Pass a glob, e.g. ${IMPECCABLE_COMMAND} hooks ignore-file "src/legacy/**"`); const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob); writeDetectorConfig(cwd, config); @@ -569,7 +570,7 @@ function parseIgnoreValueArgs(args) { function addIgnoreValue(cwd, args) { const parsed = parseIgnoreValueArgs(args); if (!parsed.rule || !parsed.value) { - throw new Error('Pass a rule id and value, e.g. /impeccable hooks ignore-value overused-font Inter'); + throw new Error(`Pass a rule id and value, e.g. ${IMPECCABLE_COMMAND} hooks ignore-value overused-font Inter`); } if (parsed.shared && parsed.local) { diff --git a/.opencode/skills/impeccable/scripts/hook-lib.mjs b/.opencode/skills/impeccable/scripts/hook-lib.mjs index 4953b91ca..0d9722953 100644 --- a/.opencode/skills/impeccable/scripts/hook-lib.mjs +++ b/.opencode/skills/impeccable/scripts/hook-lib.mjs @@ -41,6 +41,7 @@ import os from 'node:os'; import path from 'node:path'; import { pathToFileURL, fileURLToPath } from 'node:url'; import { extractPlatform, loadContext } from './context.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -661,7 +662,7 @@ export function bumpEditCount(cache, sessionId, filePath) { } export function suppressionNotice(filePath) { - return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run /impeccable audit to revisit.`; + return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run ${IMPECCABLE_COMMAND} audit to revisit.`; } // Glob → RegExp. Supports `**`, `*`, `?`, and `{a,b}` alternation. @@ -877,7 +878,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) { const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`; const lines = shown.map((f) => formatFindingLine(f)); const more = remaining > 0 - ? `... and ${remaining} more (see /impeccable audit).` + ? `... and ${remaining} more (see ${IMPECCABLE_COMMAND} audit).` : null; const footer = directiveFooter(display); @@ -921,7 +922,7 @@ function renderGroupedTemplate(groups, config, opts = {}) { shownCount += shown.length; const hidden = group.findings.length - shown.length; if (hidden > 0) { - lines.push(`- ... ${hidden} more in ${display} (see /impeccable audit).`); + lines.push(`- ... ${hidden} more in ${display} (see ${IMPECCABLE_COMMAND} audit).`); } } @@ -937,7 +938,7 @@ function clampGroupedToBudget(header, lines, footer, maxChars) { const assemble = (linesArr, omitted) => [ header, ...linesArr, - ...(omitted ? ['... and more (see /impeccable audit).'] : []), + ...(omitted ? [`... and more (see ${IMPECCABLE_COMMAND} audit).`] : []), '', footer, ].join('\n'); @@ -970,7 +971,7 @@ function clampToBudget(header, lines, more, footer, maxChars) { let assembled = assemble(working, moreText); while (assembled.length > maxChars && working.length > 1) { working.pop(); - moreText = '... and more (see /impeccable audit).'; + moreText = `... and more (see ${IMPECCABLE_COMMAND} audit).`; assembled = assemble(working, moreText); } if (assembled.length > maxChars) { @@ -1002,7 +1003,7 @@ function formatFindingIgnoreCommand(finding) { const value = extractFindingIgnoreValueRaw(finding); const valueArg = quoteCommandArg(value); const reason = quoteCommandArg(`User confirmed ${value} is intentional`); - return `/impeccable hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; + return `${IMPECCABLE_COMMAND} hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; } function quoteCommandArg(value) { @@ -1447,7 +1448,7 @@ export function designSystemOptions(config, detector, projectCwd) { export function appendDesignSystemNote(text, scanOptions) { if (!text || !scanOptions?.designSystem?.mdNewerThanJson) return text; - return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the design-system sidecar.`; + return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the design-system sidecar.`; } // The directive footer is the part of the hook output that steers model @@ -1464,16 +1465,16 @@ export function appendDesignSystemNote(text, scanOptions) { // raw envelope. Asking the model to surface the resolution in its // reply is the cheapest way to make the feedback loop visible. function directiveFooter(display, opts = {}) { - const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`; + const ignoreFileCommand = `${IMPECCABLE_COMMAND} hooks ignore-file ${quoteCommandArg(display)}`; const fileIgnoreGuidance = opts.grouped - ? 'run `/impeccable hooks ignore-file ` for the specific file' + ? `run \`${IMPECCABLE_COMMAND} hooks ignore-file \` for the specific file` : `run \`${ignoreFileCommand}\``; return [ 'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.', '', 'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.', '', - `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`, + `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`${IMPECCABLE_COMMAND} hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`${IMPECCABLE_COMMAND} hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run ${IMPECCABLE_COMMAND} audit for the full pass.`, ].join('\n'); } diff --git a/.opencode/skills/impeccable/scripts/lib/impeccable-paths.mjs b/.opencode/skills/impeccable/scripts/lib/impeccable-paths.mjs index 91121dd59..2ccbe7b74 100644 --- a/.opencode/skills/impeccable/scripts/lib/impeccable-paths.mjs +++ b/.opencode/skills/impeccable/scripts/lib/impeccable-paths.mjs @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { resolveProjectRoot } from '../context.mjs'; +export { IMPECCABLE_COMMAND_PREFIX } from './provider.mjs'; export const IMPECCABLE_DIR = '.impeccable'; export const LIVE_DIR = 'live'; diff --git a/.opencode/skills/impeccable/scripts/lib/provider.mjs b/.opencode/skills/impeccable/scripts/lib/provider.mjs new file mode 100644 index 000000000..7fa928a9c --- /dev/null +++ b/.opencode/skills/impeccable/scripts/lib/provider.mjs @@ -0,0 +1,4 @@ +// Source scripts default to slash commands. The provider build replaces only +// this exact declaration, avoiding heuristic rewrites across executable code. +export const IMPECCABLE_COMMAND_PREFIX = "/"; +export const IMPECCABLE_COMMAND = `${IMPECCABLE_COMMAND_PREFIX}impeccable`; diff --git a/.opencode/skills/impeccable/scripts/live-browser.js b/.opencode/skills/impeccable/scripts/live-browser.js index 02b8c8bcf..616f1290a 100644 --- a/.opencode/skills/impeccable/scripts/live-browser.js +++ b/.opencode/skills/impeccable/scripts/live-browser.js @@ -57,6 +57,7 @@ const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 }; const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint const PREFIX = 'impeccable-live'; + const IMPECCABLE_COMMAND = (window.__IMPECCABLE_COMMAND_PREFIX__ || '/') + 'impeccable'; const PICK_CURSOR_STYLE_ID = PREFIX + '-pick-cursor-style'; const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000; const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({ @@ -6145,7 +6146,7 @@ switch (msg.type) { case 'connected': hasProjectContext = !!msg.hasProjectContext; - if (!hasProjectContext) showToast('No PRODUCT.md found. Variants will be brand-agnostic. Run /impeccable init to generate one.', 7000); + if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000); console.log('[impeccable] Live mode connected.'); syncAgentPollingUi(!!msg.agentPolling); startAgentStatusPoll(); @@ -10538,7 +10539,7 @@ void main() { if (designState.present === false) { const empty = document.createElement('div'); empty.className = 'empty'; - empty.innerHTML = `No DESIGN.md yetCreate one by running /impeccable document in your terminal, then re-open this panel.`; + empty.innerHTML = `No DESIGN.md yetCreate one by running ${IMPECCABLE_COMMAND} document in your terminal, then re-open this panel.`; body.appendChild(empty); return; } @@ -10568,7 +10569,7 @@ void main() { box.className = 'stale'; box.innerHTML = ` - DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the sidecar. + DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the sidecar. `; return box; } @@ -10576,7 +10577,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running ${IMPECCABLE_COMMAND} document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/.opencode/skills/impeccable/scripts/live-server.mjs b/.opencode/skills/impeccable/scripts/live-server.mjs index 27005ef3c..5735f2aec 100644 --- a/.opencode/skills/impeccable/scripts/live-server.mjs +++ b/.opencode/skills/impeccable/scripts/live-server.mjs @@ -36,6 +36,7 @@ import { getDesignSidecarPath, getLiveDir, getLiveAnnotationsDir, + IMPECCABLE_COMMAND_PREFIX, readLiveServerInfo, removeLiveServerInfo, resolveDesignSidecarPath, @@ -413,6 +414,7 @@ function createRequestHandler({ detectScript, liveScriptParts }) { token: state.token, port: state.port, vocabulary: LIVE_COMMANDS, + commandPrefix: IMPECCABLE_COMMAND_PREFIX, parts, }); res.writeHead(200, { diff --git a/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs b/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs index 9229e34f8..b77f6a542 100644 --- a/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.opencode/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -32,10 +32,11 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { +export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', parts }) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + + `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; diff --git a/.opencode/skills/impeccable/scripts/pin.mjs b/.opencode/skills/impeccable/scripts/pin.mjs index b0eb39da0..52ea2701b 100644 --- a/.opencode/skills/impeccable/scripts/pin.mjs +++ b/.opencode/skills/impeccable/scripts/pin.mjs @@ -6,7 +6,7 @@ * node /pin.mjs pin * node /pin.mjs unpin * - * `pin audit` creates a lightweight /audit skill that redirects to /impeccable audit. + * `pin audit` creates a lightweight audit skill that redirects to Impeccable's audit workflow. * `unpin audit` removes that shortcut. * * The script discovers harness directories (.claude/skills, .cursor/skills, etc.) @@ -14,7 +14,7 @@ */ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs'; -import { join, resolve, dirname } from 'node:path'; +import { basename, join, resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -25,6 +25,8 @@ const HARNESS_DIRS = [ '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', ]; +const CODEX_HARNESSES = new Set(['.codex', '.agents']); + // Valid sub-command names const VALID_COMMANDS = [ 'craft', 'init', 'extract', 'document', 'shape', @@ -87,8 +89,12 @@ function loadCommandMetadata() { /** * Generate a pinned skill's SKILL.md content. */ -function generatePinnedSkill(command, metadata) { - const desc = metadata[command]?.description || `Shortcut for /impeccable ${command}.`; +function commandPrefixForSkillsDir(skillsDir) { + return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; +} + +function generatePinnedSkill(command, metadata, commandPrefix) { + const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; return `--- @@ -100,9 +106,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`/impeccable ${command}\`. +This is a pinned shortcut for \`${commandPrefix}impeccable ${command}\`. -Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } @@ -118,10 +124,11 @@ function pin(command, projectRoot) { return false; } - const content = generatePinnedSkill(command, metadata); let created = 0; for (const skillsDir of harnessDirs) { + const commandPrefix = commandPrefixForSkillsDir(skillsDir); + const content = generatePinnedSkill(command, metadata, commandPrefix); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { @@ -143,7 +150,7 @@ function pin(command, projectRoot) { if (created > 0) { console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`); - console.log(`You can now use /${command} directly.`); + console.log('Use the pinned command directly in each harness.'); } return created > 0; @@ -177,7 +184,7 @@ function unpin(command, projectRoot) { if (removed > 0) { console.log(`\nUnpinned '${command}' from ${removed} location(s).`); - console.log(`Use /impeccable ${command} to access it.`); + console.log(`Use Impeccable's '${command}' workflow directly to access it.`); } else { console.log(`No pinned '${command}' shortcut found.`); } diff --git a/.pi/skills/impeccable/scripts/context-signals.mjs b/.pi/skills/impeccable/scripts/context-signals.mjs index e12881b81..2fc27bea7 100644 --- a/.pi/skills/impeccable/scripts/context-signals.mjs +++ b/.pi/skills/impeccable/scripts/context-signals.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Context-signals gatherer for the bare `/impeccable` + * Context-signals gatherer for the bare Impeccable invocation * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.pi/skills/impeccable/scripts/context.mjs b/.pi/skills/impeccable/scripts/context.mjs index 4530f41d5..11f2aabe0 100644 --- a/.pi/skills/impeccable/scripts/context.mjs +++ b/.pi/skills/impeccable/scripts/context.mjs @@ -23,6 +23,7 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseTargetOptions } from './lib/target-args.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; @@ -902,7 +903,7 @@ async function cli() { 'or wording that clearly maps to a from-scratch build/shape flow, load ' + 'reference/init.md and write PRODUCT.md first; for any other (scoped) ' + 'command against existing code, proceed using the code as context and ' + - 'offer `/impeccable init` as a suggestion (do not block).', + `offer \`${IMPECCABLE_COMMAND} init\` as a suggestion (do not block).`, ]; parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists })); if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) { diff --git a/.pi/skills/impeccable/scripts/critique-storage.mjs b/.pi/skills/impeccable/scripts/critique-storage.mjs index 645628c8c..6b4d225cf 100644 --- a/.pi/skills/impeccable/scripts/critique-storage.mjs +++ b/.pi/skills/impeccable/scripts/critique-storage.mjs @@ -2,11 +2,11 @@ /** * Critique persistence helper. * - * Each run of /impeccable critique writes a per-target snapshot to + * Each critique run writes a per-target snapshot to * .impeccable/critique/__.md * with a small YAML frontmatter carrying the score + P0/P1 counts. * - * /impeccable polish reads the latest matching snapshot at start as its + * The polish workflow reads the latest matching snapshot at start as its * fix backlog. No other skill auto-reads critique output. * * The slug is derived mechanically from the *resolved* primary artifact diff --git a/.pi/skills/impeccable/scripts/hook-admin.mjs b/.pi/skills/impeccable/scripts/hook-admin.mjs index ce4ec7b2b..8b37b1f3b 100644 --- a/.pi/skills/impeccable/scripts/hook-admin.mjs +++ b/.pi/skills/impeccable/scripts/hook-admin.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * `/impeccable hooks ` — manage the design hook runtime + * The Impeccable hooks command manages the design hook runtime * via the `hook` key and shared detector ignores via the `detector` key in * .impeccable/config.json / .impeccable/config.local.json. * @@ -21,6 +21,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; import { getConfigPath, @@ -184,7 +185,7 @@ function writeHookConfig(cwd, hookConfig, opts = {}) { const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {}; const existingHook = stripDetectorKeys(hookSection(existing)); // Merge over the existing hook object so fields the merge helpers don't manage - // (consent, quiet, auditLog) survive a `/impeccable hooks` edit. + // (consent, quiet, auditLog) survive an Impeccable hooks edit. const next = { ...existing, hook: { ...existingHook, ...hookConfig } }; fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n'); @@ -513,9 +514,9 @@ function parseIgnoreRuleArgs(args) { function addIgnoreRule(cwd, args) { const parsed = parseIgnoreRuleArgs(args); const rule = parsed.rule; - if (!rule) throw new Error('Pass a rule id, e.g. /impeccable hooks ignore-rule side-tab'); + if (!rule) throw new Error(`Pass a rule id, e.g. ${IMPECCABLE_COMMAND} hooks ignore-rule side-tab`); if (rule === 'overused-font' && !parsed.allValues) { - throw new Error('overused-font is value-specific by default. Use /impeccable hooks ignore-value overused-font for a confirmed font, or /impeccable hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.'); + throw new Error(`overused-font is value-specific by default. Use ${IMPECCABLE_COMMAND} hooks ignore-value overused-font for a confirmed font, or ${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.`); } const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule); @@ -524,7 +525,7 @@ function addIgnoreRule(cwd, args) { } function addIgnoreFile(cwd, glob) { - if (!glob) throw new Error('Pass a glob, e.g. /impeccable hooks ignore-file "src/legacy/**"'); + if (!glob) throw new Error(`Pass a glob, e.g. ${IMPECCABLE_COMMAND} hooks ignore-file "src/legacy/**"`); const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob); writeDetectorConfig(cwd, config); @@ -569,7 +570,7 @@ function parseIgnoreValueArgs(args) { function addIgnoreValue(cwd, args) { const parsed = parseIgnoreValueArgs(args); if (!parsed.rule || !parsed.value) { - throw new Error('Pass a rule id and value, e.g. /impeccable hooks ignore-value overused-font Inter'); + throw new Error(`Pass a rule id and value, e.g. ${IMPECCABLE_COMMAND} hooks ignore-value overused-font Inter`); } if (parsed.shared && parsed.local) { diff --git a/.pi/skills/impeccable/scripts/hook-lib.mjs b/.pi/skills/impeccable/scripts/hook-lib.mjs index 4953b91ca..0d9722953 100644 --- a/.pi/skills/impeccable/scripts/hook-lib.mjs +++ b/.pi/skills/impeccable/scripts/hook-lib.mjs @@ -41,6 +41,7 @@ import os from 'node:os'; import path from 'node:path'; import { pathToFileURL, fileURLToPath } from 'node:url'; import { extractPlatform, loadContext } from './context.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -661,7 +662,7 @@ export function bumpEditCount(cache, sessionId, filePath) { } export function suppressionNotice(filePath) { - return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run /impeccable audit to revisit.`; + return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run ${IMPECCABLE_COMMAND} audit to revisit.`; } // Glob → RegExp. Supports `**`, `*`, `?`, and `{a,b}` alternation. @@ -877,7 +878,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) { const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`; const lines = shown.map((f) => formatFindingLine(f)); const more = remaining > 0 - ? `... and ${remaining} more (see /impeccable audit).` + ? `... and ${remaining} more (see ${IMPECCABLE_COMMAND} audit).` : null; const footer = directiveFooter(display); @@ -921,7 +922,7 @@ function renderGroupedTemplate(groups, config, opts = {}) { shownCount += shown.length; const hidden = group.findings.length - shown.length; if (hidden > 0) { - lines.push(`- ... ${hidden} more in ${display} (see /impeccable audit).`); + lines.push(`- ... ${hidden} more in ${display} (see ${IMPECCABLE_COMMAND} audit).`); } } @@ -937,7 +938,7 @@ function clampGroupedToBudget(header, lines, footer, maxChars) { const assemble = (linesArr, omitted) => [ header, ...linesArr, - ...(omitted ? ['... and more (see /impeccable audit).'] : []), + ...(omitted ? [`... and more (see ${IMPECCABLE_COMMAND} audit).`] : []), '', footer, ].join('\n'); @@ -970,7 +971,7 @@ function clampToBudget(header, lines, more, footer, maxChars) { let assembled = assemble(working, moreText); while (assembled.length > maxChars && working.length > 1) { working.pop(); - moreText = '... and more (see /impeccable audit).'; + moreText = `... and more (see ${IMPECCABLE_COMMAND} audit).`; assembled = assemble(working, moreText); } if (assembled.length > maxChars) { @@ -1002,7 +1003,7 @@ function formatFindingIgnoreCommand(finding) { const value = extractFindingIgnoreValueRaw(finding); const valueArg = quoteCommandArg(value); const reason = quoteCommandArg(`User confirmed ${value} is intentional`); - return `/impeccable hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; + return `${IMPECCABLE_COMMAND} hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; } function quoteCommandArg(value) { @@ -1447,7 +1448,7 @@ export function designSystemOptions(config, detector, projectCwd) { export function appendDesignSystemNote(text, scanOptions) { if (!text || !scanOptions?.designSystem?.mdNewerThanJson) return text; - return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the design-system sidecar.`; + return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the design-system sidecar.`; } // The directive footer is the part of the hook output that steers model @@ -1464,16 +1465,16 @@ export function appendDesignSystemNote(text, scanOptions) { // raw envelope. Asking the model to surface the resolution in its // reply is the cheapest way to make the feedback loop visible. function directiveFooter(display, opts = {}) { - const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`; + const ignoreFileCommand = `${IMPECCABLE_COMMAND} hooks ignore-file ${quoteCommandArg(display)}`; const fileIgnoreGuidance = opts.grouped - ? 'run `/impeccable hooks ignore-file ` for the specific file' + ? `run \`${IMPECCABLE_COMMAND} hooks ignore-file \` for the specific file` : `run \`${ignoreFileCommand}\``; return [ 'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.', '', 'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.', '', - `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`, + `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`${IMPECCABLE_COMMAND} hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`${IMPECCABLE_COMMAND} hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run ${IMPECCABLE_COMMAND} audit for the full pass.`, ].join('\n'); } diff --git a/.pi/skills/impeccable/scripts/lib/impeccable-paths.mjs b/.pi/skills/impeccable/scripts/lib/impeccable-paths.mjs index 91121dd59..2ccbe7b74 100644 --- a/.pi/skills/impeccable/scripts/lib/impeccable-paths.mjs +++ b/.pi/skills/impeccable/scripts/lib/impeccable-paths.mjs @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { resolveProjectRoot } from '../context.mjs'; +export { IMPECCABLE_COMMAND_PREFIX } from './provider.mjs'; export const IMPECCABLE_DIR = '.impeccable'; export const LIVE_DIR = 'live'; diff --git a/.pi/skills/impeccable/scripts/lib/provider.mjs b/.pi/skills/impeccable/scripts/lib/provider.mjs new file mode 100644 index 000000000..7fa928a9c --- /dev/null +++ b/.pi/skills/impeccable/scripts/lib/provider.mjs @@ -0,0 +1,4 @@ +// Source scripts default to slash commands. The provider build replaces only +// this exact declaration, avoiding heuristic rewrites across executable code. +export const IMPECCABLE_COMMAND_PREFIX = "/"; +export const IMPECCABLE_COMMAND = `${IMPECCABLE_COMMAND_PREFIX}impeccable`; diff --git a/.pi/skills/impeccable/scripts/live-browser.js b/.pi/skills/impeccable/scripts/live-browser.js index 02b8c8bcf..616f1290a 100644 --- a/.pi/skills/impeccable/scripts/live-browser.js +++ b/.pi/skills/impeccable/scripts/live-browser.js @@ -57,6 +57,7 @@ const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 }; const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint const PREFIX = 'impeccable-live'; + const IMPECCABLE_COMMAND = (window.__IMPECCABLE_COMMAND_PREFIX__ || '/') + 'impeccable'; const PICK_CURSOR_STYLE_ID = PREFIX + '-pick-cursor-style'; const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000; const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({ @@ -6145,7 +6146,7 @@ switch (msg.type) { case 'connected': hasProjectContext = !!msg.hasProjectContext; - if (!hasProjectContext) showToast('No PRODUCT.md found. Variants will be brand-agnostic. Run /impeccable init to generate one.', 7000); + if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000); console.log('[impeccable] Live mode connected.'); syncAgentPollingUi(!!msg.agentPolling); startAgentStatusPoll(); @@ -10538,7 +10539,7 @@ void main() { if (designState.present === false) { const empty = document.createElement('div'); empty.className = 'empty'; - empty.innerHTML = `No DESIGN.md yetCreate one by running /impeccable document in your terminal, then re-open this panel.`; + empty.innerHTML = `No DESIGN.md yetCreate one by running ${IMPECCABLE_COMMAND} document in your terminal, then re-open this panel.`; body.appendChild(empty); return; } @@ -10568,7 +10569,7 @@ void main() { box.className = 'stale'; box.innerHTML = ` - DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the sidecar. + DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the sidecar. `; return box; } @@ -10576,7 +10577,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running ${IMPECCABLE_COMMAND} document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/.pi/skills/impeccable/scripts/live-server.mjs b/.pi/skills/impeccable/scripts/live-server.mjs index 27005ef3c..5735f2aec 100644 --- a/.pi/skills/impeccable/scripts/live-server.mjs +++ b/.pi/skills/impeccable/scripts/live-server.mjs @@ -36,6 +36,7 @@ import { getDesignSidecarPath, getLiveDir, getLiveAnnotationsDir, + IMPECCABLE_COMMAND_PREFIX, readLiveServerInfo, removeLiveServerInfo, resolveDesignSidecarPath, @@ -413,6 +414,7 @@ function createRequestHandler({ detectScript, liveScriptParts }) { token: state.token, port: state.port, vocabulary: LIVE_COMMANDS, + commandPrefix: IMPECCABLE_COMMAND_PREFIX, parts, }); res.writeHead(200, { diff --git a/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs b/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs index 9229e34f8..b77f6a542 100644 --- a/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.pi/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -32,10 +32,11 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { +export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', parts }) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + + `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; diff --git a/.pi/skills/impeccable/scripts/pin.mjs b/.pi/skills/impeccable/scripts/pin.mjs index b0eb39da0..52ea2701b 100644 --- a/.pi/skills/impeccable/scripts/pin.mjs +++ b/.pi/skills/impeccable/scripts/pin.mjs @@ -6,7 +6,7 @@ * node /pin.mjs pin * node /pin.mjs unpin * - * `pin audit` creates a lightweight /audit skill that redirects to /impeccable audit. + * `pin audit` creates a lightweight audit skill that redirects to Impeccable's audit workflow. * `unpin audit` removes that shortcut. * * The script discovers harness directories (.claude/skills, .cursor/skills, etc.) @@ -14,7 +14,7 @@ */ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs'; -import { join, resolve, dirname } from 'node:path'; +import { basename, join, resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -25,6 +25,8 @@ const HARNESS_DIRS = [ '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', ]; +const CODEX_HARNESSES = new Set(['.codex', '.agents']); + // Valid sub-command names const VALID_COMMANDS = [ 'craft', 'init', 'extract', 'document', 'shape', @@ -87,8 +89,12 @@ function loadCommandMetadata() { /** * Generate a pinned skill's SKILL.md content. */ -function generatePinnedSkill(command, metadata) { - const desc = metadata[command]?.description || `Shortcut for /impeccable ${command}.`; +function commandPrefixForSkillsDir(skillsDir) { + return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; +} + +function generatePinnedSkill(command, metadata, commandPrefix) { + const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; return `--- @@ -100,9 +106,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`/impeccable ${command}\`. +This is a pinned shortcut for \`${commandPrefix}impeccable ${command}\`. -Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } @@ -118,10 +124,11 @@ function pin(command, projectRoot) { return false; } - const content = generatePinnedSkill(command, metadata); let created = 0; for (const skillsDir of harnessDirs) { + const commandPrefix = commandPrefixForSkillsDir(skillsDir); + const content = generatePinnedSkill(command, metadata, commandPrefix); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { @@ -143,7 +150,7 @@ function pin(command, projectRoot) { if (created > 0) { console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`); - console.log(`You can now use /${command} directly.`); + console.log('Use the pinned command directly in each harness.'); } return created > 0; @@ -177,7 +184,7 @@ function unpin(command, projectRoot) { if (removed > 0) { console.log(`\nUnpinned '${command}' from ${removed} location(s).`); - console.log(`Use /impeccable ${command} to access it.`); + console.log(`Use Impeccable's '${command}' workflow directly to access it.`); } else { console.log(`No pinned '${command}' shortcut found.`); } diff --git a/.qoder/skills/impeccable/scripts/context-signals.mjs b/.qoder/skills/impeccable/scripts/context-signals.mjs index e12881b81..2fc27bea7 100644 --- a/.qoder/skills/impeccable/scripts/context-signals.mjs +++ b/.qoder/skills/impeccable/scripts/context-signals.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Context-signals gatherer for the bare `/impeccable` + * Context-signals gatherer for the bare Impeccable invocation * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.qoder/skills/impeccable/scripts/context.mjs b/.qoder/skills/impeccable/scripts/context.mjs index 4530f41d5..11f2aabe0 100644 --- a/.qoder/skills/impeccable/scripts/context.mjs +++ b/.qoder/skills/impeccable/scripts/context.mjs @@ -23,6 +23,7 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseTargetOptions } from './lib/target-args.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; @@ -902,7 +903,7 @@ async function cli() { 'or wording that clearly maps to a from-scratch build/shape flow, load ' + 'reference/init.md and write PRODUCT.md first; for any other (scoped) ' + 'command against existing code, proceed using the code as context and ' + - 'offer `/impeccable init` as a suggestion (do not block).', + `offer \`${IMPECCABLE_COMMAND} init\` as a suggestion (do not block).`, ]; parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists })); if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) { diff --git a/.qoder/skills/impeccable/scripts/critique-storage.mjs b/.qoder/skills/impeccable/scripts/critique-storage.mjs index 645628c8c..6b4d225cf 100644 --- a/.qoder/skills/impeccable/scripts/critique-storage.mjs +++ b/.qoder/skills/impeccable/scripts/critique-storage.mjs @@ -2,11 +2,11 @@ /** * Critique persistence helper. * - * Each run of /impeccable critique writes a per-target snapshot to + * Each critique run writes a per-target snapshot to * .impeccable/critique/__.md * with a small YAML frontmatter carrying the score + P0/P1 counts. * - * /impeccable polish reads the latest matching snapshot at start as its + * The polish workflow reads the latest matching snapshot at start as its * fix backlog. No other skill auto-reads critique output. * * The slug is derived mechanically from the *resolved* primary artifact diff --git a/.qoder/skills/impeccable/scripts/hook-admin.mjs b/.qoder/skills/impeccable/scripts/hook-admin.mjs index ce4ec7b2b..8b37b1f3b 100644 --- a/.qoder/skills/impeccable/scripts/hook-admin.mjs +++ b/.qoder/skills/impeccable/scripts/hook-admin.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * `/impeccable hooks ` — manage the design hook runtime + * The Impeccable hooks command manages the design hook runtime * via the `hook` key and shared detector ignores via the `detector` key in * .impeccable/config.json / .impeccable/config.local.json. * @@ -21,6 +21,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; import { getConfigPath, @@ -184,7 +185,7 @@ function writeHookConfig(cwd, hookConfig, opts = {}) { const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {}; const existingHook = stripDetectorKeys(hookSection(existing)); // Merge over the existing hook object so fields the merge helpers don't manage - // (consent, quiet, auditLog) survive a `/impeccable hooks` edit. + // (consent, quiet, auditLog) survive an Impeccable hooks edit. const next = { ...existing, hook: { ...existingHook, ...hookConfig } }; fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n'); @@ -513,9 +514,9 @@ function parseIgnoreRuleArgs(args) { function addIgnoreRule(cwd, args) { const parsed = parseIgnoreRuleArgs(args); const rule = parsed.rule; - if (!rule) throw new Error('Pass a rule id, e.g. /impeccable hooks ignore-rule side-tab'); + if (!rule) throw new Error(`Pass a rule id, e.g. ${IMPECCABLE_COMMAND} hooks ignore-rule side-tab`); if (rule === 'overused-font' && !parsed.allValues) { - throw new Error('overused-font is value-specific by default. Use /impeccable hooks ignore-value overused-font for a confirmed font, or /impeccable hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.'); + throw new Error(`overused-font is value-specific by default. Use ${IMPECCABLE_COMMAND} hooks ignore-value overused-font for a confirmed font, or ${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.`); } const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule); @@ -524,7 +525,7 @@ function addIgnoreRule(cwd, args) { } function addIgnoreFile(cwd, glob) { - if (!glob) throw new Error('Pass a glob, e.g. /impeccable hooks ignore-file "src/legacy/**"'); + if (!glob) throw new Error(`Pass a glob, e.g. ${IMPECCABLE_COMMAND} hooks ignore-file "src/legacy/**"`); const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob); writeDetectorConfig(cwd, config); @@ -569,7 +570,7 @@ function parseIgnoreValueArgs(args) { function addIgnoreValue(cwd, args) { const parsed = parseIgnoreValueArgs(args); if (!parsed.rule || !parsed.value) { - throw new Error('Pass a rule id and value, e.g. /impeccable hooks ignore-value overused-font Inter'); + throw new Error(`Pass a rule id and value, e.g. ${IMPECCABLE_COMMAND} hooks ignore-value overused-font Inter`); } if (parsed.shared && parsed.local) { diff --git a/.qoder/skills/impeccable/scripts/hook-lib.mjs b/.qoder/skills/impeccable/scripts/hook-lib.mjs index 4953b91ca..0d9722953 100644 --- a/.qoder/skills/impeccable/scripts/hook-lib.mjs +++ b/.qoder/skills/impeccable/scripts/hook-lib.mjs @@ -41,6 +41,7 @@ import os from 'node:os'; import path from 'node:path'; import { pathToFileURL, fileURLToPath } from 'node:url'; import { extractPlatform, loadContext } from './context.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -661,7 +662,7 @@ export function bumpEditCount(cache, sessionId, filePath) { } export function suppressionNotice(filePath) { - return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run /impeccable audit to revisit.`; + return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run ${IMPECCABLE_COMMAND} audit to revisit.`; } // Glob → RegExp. Supports `**`, `*`, `?`, and `{a,b}` alternation. @@ -877,7 +878,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) { const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`; const lines = shown.map((f) => formatFindingLine(f)); const more = remaining > 0 - ? `... and ${remaining} more (see /impeccable audit).` + ? `... and ${remaining} more (see ${IMPECCABLE_COMMAND} audit).` : null; const footer = directiveFooter(display); @@ -921,7 +922,7 @@ function renderGroupedTemplate(groups, config, opts = {}) { shownCount += shown.length; const hidden = group.findings.length - shown.length; if (hidden > 0) { - lines.push(`- ... ${hidden} more in ${display} (see /impeccable audit).`); + lines.push(`- ... ${hidden} more in ${display} (see ${IMPECCABLE_COMMAND} audit).`); } } @@ -937,7 +938,7 @@ function clampGroupedToBudget(header, lines, footer, maxChars) { const assemble = (linesArr, omitted) => [ header, ...linesArr, - ...(omitted ? ['... and more (see /impeccable audit).'] : []), + ...(omitted ? [`... and more (see ${IMPECCABLE_COMMAND} audit).`] : []), '', footer, ].join('\n'); @@ -970,7 +971,7 @@ function clampToBudget(header, lines, more, footer, maxChars) { let assembled = assemble(working, moreText); while (assembled.length > maxChars && working.length > 1) { working.pop(); - moreText = '... and more (see /impeccable audit).'; + moreText = `... and more (see ${IMPECCABLE_COMMAND} audit).`; assembled = assemble(working, moreText); } if (assembled.length > maxChars) { @@ -1002,7 +1003,7 @@ function formatFindingIgnoreCommand(finding) { const value = extractFindingIgnoreValueRaw(finding); const valueArg = quoteCommandArg(value); const reason = quoteCommandArg(`User confirmed ${value} is intentional`); - return `/impeccable hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; + return `${IMPECCABLE_COMMAND} hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; } function quoteCommandArg(value) { @@ -1447,7 +1448,7 @@ export function designSystemOptions(config, detector, projectCwd) { export function appendDesignSystemNote(text, scanOptions) { if (!text || !scanOptions?.designSystem?.mdNewerThanJson) return text; - return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the design-system sidecar.`; + return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the design-system sidecar.`; } // The directive footer is the part of the hook output that steers model @@ -1464,16 +1465,16 @@ export function appendDesignSystemNote(text, scanOptions) { // raw envelope. Asking the model to surface the resolution in its // reply is the cheapest way to make the feedback loop visible. function directiveFooter(display, opts = {}) { - const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`; + const ignoreFileCommand = `${IMPECCABLE_COMMAND} hooks ignore-file ${quoteCommandArg(display)}`; const fileIgnoreGuidance = opts.grouped - ? 'run `/impeccable hooks ignore-file ` for the specific file' + ? `run \`${IMPECCABLE_COMMAND} hooks ignore-file \` for the specific file` : `run \`${ignoreFileCommand}\``; return [ 'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.', '', 'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.', '', - `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`, + `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`${IMPECCABLE_COMMAND} hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`${IMPECCABLE_COMMAND} hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run ${IMPECCABLE_COMMAND} audit for the full pass.`, ].join('\n'); } diff --git a/.qoder/skills/impeccable/scripts/lib/impeccable-paths.mjs b/.qoder/skills/impeccable/scripts/lib/impeccable-paths.mjs index 91121dd59..2ccbe7b74 100644 --- a/.qoder/skills/impeccable/scripts/lib/impeccable-paths.mjs +++ b/.qoder/skills/impeccable/scripts/lib/impeccable-paths.mjs @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { resolveProjectRoot } from '../context.mjs'; +export { IMPECCABLE_COMMAND_PREFIX } from './provider.mjs'; export const IMPECCABLE_DIR = '.impeccable'; export const LIVE_DIR = 'live'; diff --git a/.qoder/skills/impeccable/scripts/lib/provider.mjs b/.qoder/skills/impeccable/scripts/lib/provider.mjs new file mode 100644 index 000000000..7fa928a9c --- /dev/null +++ b/.qoder/skills/impeccable/scripts/lib/provider.mjs @@ -0,0 +1,4 @@ +// Source scripts default to slash commands. The provider build replaces only +// this exact declaration, avoiding heuristic rewrites across executable code. +export const IMPECCABLE_COMMAND_PREFIX = "/"; +export const IMPECCABLE_COMMAND = `${IMPECCABLE_COMMAND_PREFIX}impeccable`; diff --git a/.qoder/skills/impeccable/scripts/live-browser.js b/.qoder/skills/impeccable/scripts/live-browser.js index 02b8c8bcf..616f1290a 100644 --- a/.qoder/skills/impeccable/scripts/live-browser.js +++ b/.qoder/skills/impeccable/scripts/live-browser.js @@ -57,6 +57,7 @@ const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 }; const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint const PREFIX = 'impeccable-live'; + const IMPECCABLE_COMMAND = (window.__IMPECCABLE_COMMAND_PREFIX__ || '/') + 'impeccable'; const PICK_CURSOR_STYLE_ID = PREFIX + '-pick-cursor-style'; const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000; const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({ @@ -6145,7 +6146,7 @@ switch (msg.type) { case 'connected': hasProjectContext = !!msg.hasProjectContext; - if (!hasProjectContext) showToast('No PRODUCT.md found. Variants will be brand-agnostic. Run /impeccable init to generate one.', 7000); + if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000); console.log('[impeccable] Live mode connected.'); syncAgentPollingUi(!!msg.agentPolling); startAgentStatusPoll(); @@ -10538,7 +10539,7 @@ void main() { if (designState.present === false) { const empty = document.createElement('div'); empty.className = 'empty'; - empty.innerHTML = `No DESIGN.md yetCreate one by running /impeccable document in your terminal, then re-open this panel.`; + empty.innerHTML = `No DESIGN.md yetCreate one by running ${IMPECCABLE_COMMAND} document in your terminal, then re-open this panel.`; body.appendChild(empty); return; } @@ -10568,7 +10569,7 @@ void main() { box.className = 'stale'; box.innerHTML = ` - DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the sidecar. + DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the sidecar. `; return box; } @@ -10576,7 +10577,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running ${IMPECCABLE_COMMAND} document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/.qoder/skills/impeccable/scripts/live-server.mjs b/.qoder/skills/impeccable/scripts/live-server.mjs index 27005ef3c..5735f2aec 100644 --- a/.qoder/skills/impeccable/scripts/live-server.mjs +++ b/.qoder/skills/impeccable/scripts/live-server.mjs @@ -36,6 +36,7 @@ import { getDesignSidecarPath, getLiveDir, getLiveAnnotationsDir, + IMPECCABLE_COMMAND_PREFIX, readLiveServerInfo, removeLiveServerInfo, resolveDesignSidecarPath, @@ -413,6 +414,7 @@ function createRequestHandler({ detectScript, liveScriptParts }) { token: state.token, port: state.port, vocabulary: LIVE_COMMANDS, + commandPrefix: IMPECCABLE_COMMAND_PREFIX, parts, }); res.writeHead(200, { diff --git a/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs b/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs index 9229e34f8..b77f6a542 100644 --- a/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.qoder/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -32,10 +32,11 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { +export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', parts }) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + + `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; diff --git a/.qoder/skills/impeccable/scripts/pin.mjs b/.qoder/skills/impeccable/scripts/pin.mjs index b0eb39da0..52ea2701b 100644 --- a/.qoder/skills/impeccable/scripts/pin.mjs +++ b/.qoder/skills/impeccable/scripts/pin.mjs @@ -6,7 +6,7 @@ * node /pin.mjs pin * node /pin.mjs unpin * - * `pin audit` creates a lightweight /audit skill that redirects to /impeccable audit. + * `pin audit` creates a lightweight audit skill that redirects to Impeccable's audit workflow. * `unpin audit` removes that shortcut. * * The script discovers harness directories (.claude/skills, .cursor/skills, etc.) @@ -14,7 +14,7 @@ */ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs'; -import { join, resolve, dirname } from 'node:path'; +import { basename, join, resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -25,6 +25,8 @@ const HARNESS_DIRS = [ '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', ]; +const CODEX_HARNESSES = new Set(['.codex', '.agents']); + // Valid sub-command names const VALID_COMMANDS = [ 'craft', 'init', 'extract', 'document', 'shape', @@ -87,8 +89,12 @@ function loadCommandMetadata() { /** * Generate a pinned skill's SKILL.md content. */ -function generatePinnedSkill(command, metadata) { - const desc = metadata[command]?.description || `Shortcut for /impeccable ${command}.`; +function commandPrefixForSkillsDir(skillsDir) { + return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; +} + +function generatePinnedSkill(command, metadata, commandPrefix) { + const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; return `--- @@ -100,9 +106,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`/impeccable ${command}\`. +This is a pinned shortcut for \`${commandPrefix}impeccable ${command}\`. -Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } @@ -118,10 +124,11 @@ function pin(command, projectRoot) { return false; } - const content = generatePinnedSkill(command, metadata); let created = 0; for (const skillsDir of harnessDirs) { + const commandPrefix = commandPrefixForSkillsDir(skillsDir); + const content = generatePinnedSkill(command, metadata, commandPrefix); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { @@ -143,7 +150,7 @@ function pin(command, projectRoot) { if (created > 0) { console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`); - console.log(`You can now use /${command} directly.`); + console.log('Use the pinned command directly in each harness.'); } return created > 0; @@ -177,7 +184,7 @@ function unpin(command, projectRoot) { if (removed > 0) { console.log(`\nUnpinned '${command}' from ${removed} location(s).`); - console.log(`Use /impeccable ${command} to access it.`); + console.log(`Use Impeccable's '${command}' workflow directly to access it.`); } else { console.log(`No pinned '${command}' shortcut found.`); } diff --git a/.rovodev/skills/impeccable/scripts/context-signals.mjs b/.rovodev/skills/impeccable/scripts/context-signals.mjs index e12881b81..2fc27bea7 100644 --- a/.rovodev/skills/impeccable/scripts/context-signals.mjs +++ b/.rovodev/skills/impeccable/scripts/context-signals.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Context-signals gatherer for the bare `/impeccable` + * Context-signals gatherer for the bare Impeccable invocation * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.rovodev/skills/impeccable/scripts/context.mjs b/.rovodev/skills/impeccable/scripts/context.mjs index 4530f41d5..11f2aabe0 100644 --- a/.rovodev/skills/impeccable/scripts/context.mjs +++ b/.rovodev/skills/impeccable/scripts/context.mjs @@ -23,6 +23,7 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseTargetOptions } from './lib/target-args.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; @@ -902,7 +903,7 @@ async function cli() { 'or wording that clearly maps to a from-scratch build/shape flow, load ' + 'reference/init.md and write PRODUCT.md first; for any other (scoped) ' + 'command against existing code, proceed using the code as context and ' + - 'offer `/impeccable init` as a suggestion (do not block).', + `offer \`${IMPECCABLE_COMMAND} init\` as a suggestion (do not block).`, ]; parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists })); if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) { diff --git a/.rovodev/skills/impeccable/scripts/critique-storage.mjs b/.rovodev/skills/impeccable/scripts/critique-storage.mjs index 645628c8c..6b4d225cf 100644 --- a/.rovodev/skills/impeccable/scripts/critique-storage.mjs +++ b/.rovodev/skills/impeccable/scripts/critique-storage.mjs @@ -2,11 +2,11 @@ /** * Critique persistence helper. * - * Each run of /impeccable critique writes a per-target snapshot to + * Each critique run writes a per-target snapshot to * .impeccable/critique/__.md * with a small YAML frontmatter carrying the score + P0/P1 counts. * - * /impeccable polish reads the latest matching snapshot at start as its + * The polish workflow reads the latest matching snapshot at start as its * fix backlog. No other skill auto-reads critique output. * * The slug is derived mechanically from the *resolved* primary artifact diff --git a/.rovodev/skills/impeccable/scripts/hook-admin.mjs b/.rovodev/skills/impeccable/scripts/hook-admin.mjs index ce4ec7b2b..8b37b1f3b 100644 --- a/.rovodev/skills/impeccable/scripts/hook-admin.mjs +++ b/.rovodev/skills/impeccable/scripts/hook-admin.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * `/impeccable hooks ` — manage the design hook runtime + * The Impeccable hooks command manages the design hook runtime * via the `hook` key and shared detector ignores via the `detector` key in * .impeccable/config.json / .impeccable/config.local.json. * @@ -21,6 +21,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; import { getConfigPath, @@ -184,7 +185,7 @@ function writeHookConfig(cwd, hookConfig, opts = {}) { const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {}; const existingHook = stripDetectorKeys(hookSection(existing)); // Merge over the existing hook object so fields the merge helpers don't manage - // (consent, quiet, auditLog) survive a `/impeccable hooks` edit. + // (consent, quiet, auditLog) survive an Impeccable hooks edit. const next = { ...existing, hook: { ...existingHook, ...hookConfig } }; fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n'); @@ -513,9 +514,9 @@ function parseIgnoreRuleArgs(args) { function addIgnoreRule(cwd, args) { const parsed = parseIgnoreRuleArgs(args); const rule = parsed.rule; - if (!rule) throw new Error('Pass a rule id, e.g. /impeccable hooks ignore-rule side-tab'); + if (!rule) throw new Error(`Pass a rule id, e.g. ${IMPECCABLE_COMMAND} hooks ignore-rule side-tab`); if (rule === 'overused-font' && !parsed.allValues) { - throw new Error('overused-font is value-specific by default. Use /impeccable hooks ignore-value overused-font for a confirmed font, or /impeccable hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.'); + throw new Error(`overused-font is value-specific by default. Use ${IMPECCABLE_COMMAND} hooks ignore-value overused-font for a confirmed font, or ${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.`); } const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule); @@ -524,7 +525,7 @@ function addIgnoreRule(cwd, args) { } function addIgnoreFile(cwd, glob) { - if (!glob) throw new Error('Pass a glob, e.g. /impeccable hooks ignore-file "src/legacy/**"'); + if (!glob) throw new Error(`Pass a glob, e.g. ${IMPECCABLE_COMMAND} hooks ignore-file "src/legacy/**"`); const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob); writeDetectorConfig(cwd, config); @@ -569,7 +570,7 @@ function parseIgnoreValueArgs(args) { function addIgnoreValue(cwd, args) { const parsed = parseIgnoreValueArgs(args); if (!parsed.rule || !parsed.value) { - throw new Error('Pass a rule id and value, e.g. /impeccable hooks ignore-value overused-font Inter'); + throw new Error(`Pass a rule id and value, e.g. ${IMPECCABLE_COMMAND} hooks ignore-value overused-font Inter`); } if (parsed.shared && parsed.local) { diff --git a/.rovodev/skills/impeccable/scripts/hook-lib.mjs b/.rovodev/skills/impeccable/scripts/hook-lib.mjs index 4953b91ca..0d9722953 100644 --- a/.rovodev/skills/impeccable/scripts/hook-lib.mjs +++ b/.rovodev/skills/impeccable/scripts/hook-lib.mjs @@ -41,6 +41,7 @@ import os from 'node:os'; import path from 'node:path'; import { pathToFileURL, fileURLToPath } from 'node:url'; import { extractPlatform, loadContext } from './context.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -661,7 +662,7 @@ export function bumpEditCount(cache, sessionId, filePath) { } export function suppressionNotice(filePath) { - return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run /impeccable audit to revisit.`; + return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run ${IMPECCABLE_COMMAND} audit to revisit.`; } // Glob → RegExp. Supports `**`, `*`, `?`, and `{a,b}` alternation. @@ -877,7 +878,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) { const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`; const lines = shown.map((f) => formatFindingLine(f)); const more = remaining > 0 - ? `... and ${remaining} more (see /impeccable audit).` + ? `... and ${remaining} more (see ${IMPECCABLE_COMMAND} audit).` : null; const footer = directiveFooter(display); @@ -921,7 +922,7 @@ function renderGroupedTemplate(groups, config, opts = {}) { shownCount += shown.length; const hidden = group.findings.length - shown.length; if (hidden > 0) { - lines.push(`- ... ${hidden} more in ${display} (see /impeccable audit).`); + lines.push(`- ... ${hidden} more in ${display} (see ${IMPECCABLE_COMMAND} audit).`); } } @@ -937,7 +938,7 @@ function clampGroupedToBudget(header, lines, footer, maxChars) { const assemble = (linesArr, omitted) => [ header, ...linesArr, - ...(omitted ? ['... and more (see /impeccable audit).'] : []), + ...(omitted ? [`... and more (see ${IMPECCABLE_COMMAND} audit).`] : []), '', footer, ].join('\n'); @@ -970,7 +971,7 @@ function clampToBudget(header, lines, more, footer, maxChars) { let assembled = assemble(working, moreText); while (assembled.length > maxChars && working.length > 1) { working.pop(); - moreText = '... and more (see /impeccable audit).'; + moreText = `... and more (see ${IMPECCABLE_COMMAND} audit).`; assembled = assemble(working, moreText); } if (assembled.length > maxChars) { @@ -1002,7 +1003,7 @@ function formatFindingIgnoreCommand(finding) { const value = extractFindingIgnoreValueRaw(finding); const valueArg = quoteCommandArg(value); const reason = quoteCommandArg(`User confirmed ${value} is intentional`); - return `/impeccable hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; + return `${IMPECCABLE_COMMAND} hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; } function quoteCommandArg(value) { @@ -1447,7 +1448,7 @@ export function designSystemOptions(config, detector, projectCwd) { export function appendDesignSystemNote(text, scanOptions) { if (!text || !scanOptions?.designSystem?.mdNewerThanJson) return text; - return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the design-system sidecar.`; + return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the design-system sidecar.`; } // The directive footer is the part of the hook output that steers model @@ -1464,16 +1465,16 @@ export function appendDesignSystemNote(text, scanOptions) { // raw envelope. Asking the model to surface the resolution in its // reply is the cheapest way to make the feedback loop visible. function directiveFooter(display, opts = {}) { - const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`; + const ignoreFileCommand = `${IMPECCABLE_COMMAND} hooks ignore-file ${quoteCommandArg(display)}`; const fileIgnoreGuidance = opts.grouped - ? 'run `/impeccable hooks ignore-file ` for the specific file' + ? `run \`${IMPECCABLE_COMMAND} hooks ignore-file \` for the specific file` : `run \`${ignoreFileCommand}\``; return [ 'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.', '', 'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.', '', - `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`, + `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`${IMPECCABLE_COMMAND} hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`${IMPECCABLE_COMMAND} hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run ${IMPECCABLE_COMMAND} audit for the full pass.`, ].join('\n'); } diff --git a/.rovodev/skills/impeccable/scripts/lib/impeccable-paths.mjs b/.rovodev/skills/impeccable/scripts/lib/impeccable-paths.mjs index 91121dd59..2ccbe7b74 100644 --- a/.rovodev/skills/impeccable/scripts/lib/impeccable-paths.mjs +++ b/.rovodev/skills/impeccable/scripts/lib/impeccable-paths.mjs @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { resolveProjectRoot } from '../context.mjs'; +export { IMPECCABLE_COMMAND_PREFIX } from './provider.mjs'; export const IMPECCABLE_DIR = '.impeccable'; export const LIVE_DIR = 'live'; diff --git a/.rovodev/skills/impeccable/scripts/lib/provider.mjs b/.rovodev/skills/impeccable/scripts/lib/provider.mjs new file mode 100644 index 000000000..7fa928a9c --- /dev/null +++ b/.rovodev/skills/impeccable/scripts/lib/provider.mjs @@ -0,0 +1,4 @@ +// Source scripts default to slash commands. The provider build replaces only +// this exact declaration, avoiding heuristic rewrites across executable code. +export const IMPECCABLE_COMMAND_PREFIX = "/"; +export const IMPECCABLE_COMMAND = `${IMPECCABLE_COMMAND_PREFIX}impeccable`; diff --git a/.rovodev/skills/impeccable/scripts/live-browser.js b/.rovodev/skills/impeccable/scripts/live-browser.js index 02b8c8bcf..616f1290a 100644 --- a/.rovodev/skills/impeccable/scripts/live-browser.js +++ b/.rovodev/skills/impeccable/scripts/live-browser.js @@ -57,6 +57,7 @@ const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 }; const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint const PREFIX = 'impeccable-live'; + const IMPECCABLE_COMMAND = (window.__IMPECCABLE_COMMAND_PREFIX__ || '/') + 'impeccable'; const PICK_CURSOR_STYLE_ID = PREFIX + '-pick-cursor-style'; const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000; const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({ @@ -6145,7 +6146,7 @@ switch (msg.type) { case 'connected': hasProjectContext = !!msg.hasProjectContext; - if (!hasProjectContext) showToast('No PRODUCT.md found. Variants will be brand-agnostic. Run /impeccable init to generate one.', 7000); + if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000); console.log('[impeccable] Live mode connected.'); syncAgentPollingUi(!!msg.agentPolling); startAgentStatusPoll(); @@ -10538,7 +10539,7 @@ void main() { if (designState.present === false) { const empty = document.createElement('div'); empty.className = 'empty'; - empty.innerHTML = `No DESIGN.md yetCreate one by running /impeccable document in your terminal, then re-open this panel.`; + empty.innerHTML = `No DESIGN.md yetCreate one by running ${IMPECCABLE_COMMAND} document in your terminal, then re-open this panel.`; body.appendChild(empty); return; } @@ -10568,7 +10569,7 @@ void main() { box.className = 'stale'; box.innerHTML = ` - DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the sidecar. + DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the sidecar. `; return box; } @@ -10576,7 +10577,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running ${IMPECCABLE_COMMAND} document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/.rovodev/skills/impeccable/scripts/live-server.mjs b/.rovodev/skills/impeccable/scripts/live-server.mjs index 27005ef3c..5735f2aec 100644 --- a/.rovodev/skills/impeccable/scripts/live-server.mjs +++ b/.rovodev/skills/impeccable/scripts/live-server.mjs @@ -36,6 +36,7 @@ import { getDesignSidecarPath, getLiveDir, getLiveAnnotationsDir, + IMPECCABLE_COMMAND_PREFIX, readLiveServerInfo, removeLiveServerInfo, resolveDesignSidecarPath, @@ -413,6 +414,7 @@ function createRequestHandler({ detectScript, liveScriptParts }) { token: state.token, port: state.port, vocabulary: LIVE_COMMANDS, + commandPrefix: IMPECCABLE_COMMAND_PREFIX, parts, }); res.writeHead(200, { diff --git a/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs b/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs index 9229e34f8..b77f6a542 100644 --- a/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.rovodev/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -32,10 +32,11 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { +export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', parts }) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + + `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; diff --git a/.rovodev/skills/impeccable/scripts/pin.mjs b/.rovodev/skills/impeccable/scripts/pin.mjs index b0eb39da0..52ea2701b 100644 --- a/.rovodev/skills/impeccable/scripts/pin.mjs +++ b/.rovodev/skills/impeccable/scripts/pin.mjs @@ -6,7 +6,7 @@ * node /pin.mjs pin * node /pin.mjs unpin * - * `pin audit` creates a lightweight /audit skill that redirects to /impeccable audit. + * `pin audit` creates a lightweight audit skill that redirects to Impeccable's audit workflow. * `unpin audit` removes that shortcut. * * The script discovers harness directories (.claude/skills, .cursor/skills, etc.) @@ -14,7 +14,7 @@ */ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs'; -import { join, resolve, dirname } from 'node:path'; +import { basename, join, resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -25,6 +25,8 @@ const HARNESS_DIRS = [ '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', ]; +const CODEX_HARNESSES = new Set(['.codex', '.agents']); + // Valid sub-command names const VALID_COMMANDS = [ 'craft', 'init', 'extract', 'document', 'shape', @@ -87,8 +89,12 @@ function loadCommandMetadata() { /** * Generate a pinned skill's SKILL.md content. */ -function generatePinnedSkill(command, metadata) { - const desc = metadata[command]?.description || `Shortcut for /impeccable ${command}.`; +function commandPrefixForSkillsDir(skillsDir) { + return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; +} + +function generatePinnedSkill(command, metadata, commandPrefix) { + const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; return `--- @@ -100,9 +106,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`/impeccable ${command}\`. +This is a pinned shortcut for \`${commandPrefix}impeccable ${command}\`. -Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } @@ -118,10 +124,11 @@ function pin(command, projectRoot) { return false; } - const content = generatePinnedSkill(command, metadata); let created = 0; for (const skillsDir of harnessDirs) { + const commandPrefix = commandPrefixForSkillsDir(skillsDir); + const content = generatePinnedSkill(command, metadata, commandPrefix); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { @@ -143,7 +150,7 @@ function pin(command, projectRoot) { if (created > 0) { console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`); - console.log(`You can now use /${command} directly.`); + console.log('Use the pinned command directly in each harness.'); } return created > 0; @@ -177,7 +184,7 @@ function unpin(command, projectRoot) { if (removed > 0) { console.log(`\nUnpinned '${command}' from ${removed} location(s).`); - console.log(`Use /impeccable ${command} to access it.`); + console.log(`Use Impeccable's '${command}' workflow directly to access it.`); } else { console.log(`No pinned '${command}' shortcut found.`); } diff --git a/.trae-cn/skills/impeccable/scripts/context-signals.mjs b/.trae-cn/skills/impeccable/scripts/context-signals.mjs index e12881b81..2fc27bea7 100644 --- a/.trae-cn/skills/impeccable/scripts/context-signals.mjs +++ b/.trae-cn/skills/impeccable/scripts/context-signals.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Context-signals gatherer for the bare `/impeccable` + * Context-signals gatherer for the bare Impeccable invocation * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.trae-cn/skills/impeccable/scripts/context.mjs b/.trae-cn/skills/impeccable/scripts/context.mjs index 4530f41d5..11f2aabe0 100644 --- a/.trae-cn/skills/impeccable/scripts/context.mjs +++ b/.trae-cn/skills/impeccable/scripts/context.mjs @@ -23,6 +23,7 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseTargetOptions } from './lib/target-args.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; @@ -902,7 +903,7 @@ async function cli() { 'or wording that clearly maps to a from-scratch build/shape flow, load ' + 'reference/init.md and write PRODUCT.md first; for any other (scoped) ' + 'command against existing code, proceed using the code as context and ' + - 'offer `/impeccable init` as a suggestion (do not block).', + `offer \`${IMPECCABLE_COMMAND} init\` as a suggestion (do not block).`, ]; parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists })); if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) { diff --git a/.trae-cn/skills/impeccable/scripts/critique-storage.mjs b/.trae-cn/skills/impeccable/scripts/critique-storage.mjs index 645628c8c..6b4d225cf 100644 --- a/.trae-cn/skills/impeccable/scripts/critique-storage.mjs +++ b/.trae-cn/skills/impeccable/scripts/critique-storage.mjs @@ -2,11 +2,11 @@ /** * Critique persistence helper. * - * Each run of /impeccable critique writes a per-target snapshot to + * Each critique run writes a per-target snapshot to * .impeccable/critique/__.md * with a small YAML frontmatter carrying the score + P0/P1 counts. * - * /impeccable polish reads the latest matching snapshot at start as its + * The polish workflow reads the latest matching snapshot at start as its * fix backlog. No other skill auto-reads critique output. * * The slug is derived mechanically from the *resolved* primary artifact diff --git a/.trae-cn/skills/impeccable/scripts/hook-admin.mjs b/.trae-cn/skills/impeccable/scripts/hook-admin.mjs index ce4ec7b2b..8b37b1f3b 100644 --- a/.trae-cn/skills/impeccable/scripts/hook-admin.mjs +++ b/.trae-cn/skills/impeccable/scripts/hook-admin.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * `/impeccable hooks ` — manage the design hook runtime + * The Impeccable hooks command manages the design hook runtime * via the `hook` key and shared detector ignores via the `detector` key in * .impeccable/config.json / .impeccable/config.local.json. * @@ -21,6 +21,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; import { getConfigPath, @@ -184,7 +185,7 @@ function writeHookConfig(cwd, hookConfig, opts = {}) { const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {}; const existingHook = stripDetectorKeys(hookSection(existing)); // Merge over the existing hook object so fields the merge helpers don't manage - // (consent, quiet, auditLog) survive a `/impeccable hooks` edit. + // (consent, quiet, auditLog) survive an Impeccable hooks edit. const next = { ...existing, hook: { ...existingHook, ...hookConfig } }; fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n'); @@ -513,9 +514,9 @@ function parseIgnoreRuleArgs(args) { function addIgnoreRule(cwd, args) { const parsed = parseIgnoreRuleArgs(args); const rule = parsed.rule; - if (!rule) throw new Error('Pass a rule id, e.g. /impeccable hooks ignore-rule side-tab'); + if (!rule) throw new Error(`Pass a rule id, e.g. ${IMPECCABLE_COMMAND} hooks ignore-rule side-tab`); if (rule === 'overused-font' && !parsed.allValues) { - throw new Error('overused-font is value-specific by default. Use /impeccable hooks ignore-value overused-font for a confirmed font, or /impeccable hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.'); + throw new Error(`overused-font is value-specific by default. Use ${IMPECCABLE_COMMAND} hooks ignore-value overused-font for a confirmed font, or ${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.`); } const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule); @@ -524,7 +525,7 @@ function addIgnoreRule(cwd, args) { } function addIgnoreFile(cwd, glob) { - if (!glob) throw new Error('Pass a glob, e.g. /impeccable hooks ignore-file "src/legacy/**"'); + if (!glob) throw new Error(`Pass a glob, e.g. ${IMPECCABLE_COMMAND} hooks ignore-file "src/legacy/**"`); const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob); writeDetectorConfig(cwd, config); @@ -569,7 +570,7 @@ function parseIgnoreValueArgs(args) { function addIgnoreValue(cwd, args) { const parsed = parseIgnoreValueArgs(args); if (!parsed.rule || !parsed.value) { - throw new Error('Pass a rule id and value, e.g. /impeccable hooks ignore-value overused-font Inter'); + throw new Error(`Pass a rule id and value, e.g. ${IMPECCABLE_COMMAND} hooks ignore-value overused-font Inter`); } if (parsed.shared && parsed.local) { diff --git a/.trae-cn/skills/impeccable/scripts/hook-lib.mjs b/.trae-cn/skills/impeccable/scripts/hook-lib.mjs index 4953b91ca..0d9722953 100644 --- a/.trae-cn/skills/impeccable/scripts/hook-lib.mjs +++ b/.trae-cn/skills/impeccable/scripts/hook-lib.mjs @@ -41,6 +41,7 @@ import os from 'node:os'; import path from 'node:path'; import { pathToFileURL, fileURLToPath } from 'node:url'; import { extractPlatform, loadContext } from './context.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -661,7 +662,7 @@ export function bumpEditCount(cache, sessionId, filePath) { } export function suppressionNotice(filePath) { - return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run /impeccable audit to revisit.`; + return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run ${IMPECCABLE_COMMAND} audit to revisit.`; } // Glob → RegExp. Supports `**`, `*`, `?`, and `{a,b}` alternation. @@ -877,7 +878,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) { const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`; const lines = shown.map((f) => formatFindingLine(f)); const more = remaining > 0 - ? `... and ${remaining} more (see /impeccable audit).` + ? `... and ${remaining} more (see ${IMPECCABLE_COMMAND} audit).` : null; const footer = directiveFooter(display); @@ -921,7 +922,7 @@ function renderGroupedTemplate(groups, config, opts = {}) { shownCount += shown.length; const hidden = group.findings.length - shown.length; if (hidden > 0) { - lines.push(`- ... ${hidden} more in ${display} (see /impeccable audit).`); + lines.push(`- ... ${hidden} more in ${display} (see ${IMPECCABLE_COMMAND} audit).`); } } @@ -937,7 +938,7 @@ function clampGroupedToBudget(header, lines, footer, maxChars) { const assemble = (linesArr, omitted) => [ header, ...linesArr, - ...(omitted ? ['... and more (see /impeccable audit).'] : []), + ...(omitted ? [`... and more (see ${IMPECCABLE_COMMAND} audit).`] : []), '', footer, ].join('\n'); @@ -970,7 +971,7 @@ function clampToBudget(header, lines, more, footer, maxChars) { let assembled = assemble(working, moreText); while (assembled.length > maxChars && working.length > 1) { working.pop(); - moreText = '... and more (see /impeccable audit).'; + moreText = `... and more (see ${IMPECCABLE_COMMAND} audit).`; assembled = assemble(working, moreText); } if (assembled.length > maxChars) { @@ -1002,7 +1003,7 @@ function formatFindingIgnoreCommand(finding) { const value = extractFindingIgnoreValueRaw(finding); const valueArg = quoteCommandArg(value); const reason = quoteCommandArg(`User confirmed ${value} is intentional`); - return `/impeccable hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; + return `${IMPECCABLE_COMMAND} hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; } function quoteCommandArg(value) { @@ -1447,7 +1448,7 @@ export function designSystemOptions(config, detector, projectCwd) { export function appendDesignSystemNote(text, scanOptions) { if (!text || !scanOptions?.designSystem?.mdNewerThanJson) return text; - return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the design-system sidecar.`; + return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the design-system sidecar.`; } // The directive footer is the part of the hook output that steers model @@ -1464,16 +1465,16 @@ export function appendDesignSystemNote(text, scanOptions) { // raw envelope. Asking the model to surface the resolution in its // reply is the cheapest way to make the feedback loop visible. function directiveFooter(display, opts = {}) { - const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`; + const ignoreFileCommand = `${IMPECCABLE_COMMAND} hooks ignore-file ${quoteCommandArg(display)}`; const fileIgnoreGuidance = opts.grouped - ? 'run `/impeccable hooks ignore-file ` for the specific file' + ? `run \`${IMPECCABLE_COMMAND} hooks ignore-file \` for the specific file` : `run \`${ignoreFileCommand}\``; return [ 'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.', '', 'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.', '', - `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`, + `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`${IMPECCABLE_COMMAND} hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`${IMPECCABLE_COMMAND} hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run ${IMPECCABLE_COMMAND} audit for the full pass.`, ].join('\n'); } diff --git a/.trae-cn/skills/impeccable/scripts/lib/impeccable-paths.mjs b/.trae-cn/skills/impeccable/scripts/lib/impeccable-paths.mjs index 91121dd59..2ccbe7b74 100644 --- a/.trae-cn/skills/impeccable/scripts/lib/impeccable-paths.mjs +++ b/.trae-cn/skills/impeccable/scripts/lib/impeccable-paths.mjs @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { resolveProjectRoot } from '../context.mjs'; +export { IMPECCABLE_COMMAND_PREFIX } from './provider.mjs'; export const IMPECCABLE_DIR = '.impeccable'; export const LIVE_DIR = 'live'; diff --git a/.trae-cn/skills/impeccable/scripts/lib/provider.mjs b/.trae-cn/skills/impeccable/scripts/lib/provider.mjs new file mode 100644 index 000000000..7fa928a9c --- /dev/null +++ b/.trae-cn/skills/impeccable/scripts/lib/provider.mjs @@ -0,0 +1,4 @@ +// Source scripts default to slash commands. The provider build replaces only +// this exact declaration, avoiding heuristic rewrites across executable code. +export const IMPECCABLE_COMMAND_PREFIX = "/"; +export const IMPECCABLE_COMMAND = `${IMPECCABLE_COMMAND_PREFIX}impeccable`; diff --git a/.trae-cn/skills/impeccable/scripts/live-browser.js b/.trae-cn/skills/impeccable/scripts/live-browser.js index 02b8c8bcf..616f1290a 100644 --- a/.trae-cn/skills/impeccable/scripts/live-browser.js +++ b/.trae-cn/skills/impeccable/scripts/live-browser.js @@ -57,6 +57,7 @@ const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 }; const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint const PREFIX = 'impeccable-live'; + const IMPECCABLE_COMMAND = (window.__IMPECCABLE_COMMAND_PREFIX__ || '/') + 'impeccable'; const PICK_CURSOR_STYLE_ID = PREFIX + '-pick-cursor-style'; const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000; const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({ @@ -6145,7 +6146,7 @@ switch (msg.type) { case 'connected': hasProjectContext = !!msg.hasProjectContext; - if (!hasProjectContext) showToast('No PRODUCT.md found. Variants will be brand-agnostic. Run /impeccable init to generate one.', 7000); + if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000); console.log('[impeccable] Live mode connected.'); syncAgentPollingUi(!!msg.agentPolling); startAgentStatusPoll(); @@ -10538,7 +10539,7 @@ void main() { if (designState.present === false) { const empty = document.createElement('div'); empty.className = 'empty'; - empty.innerHTML = `No DESIGN.md yetCreate one by running /impeccable document in your terminal, then re-open this panel.`; + empty.innerHTML = `No DESIGN.md yetCreate one by running ${IMPECCABLE_COMMAND} document in your terminal, then re-open this panel.`; body.appendChild(empty); return; } @@ -10568,7 +10569,7 @@ void main() { box.className = 'stale'; box.innerHTML = ` - DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the sidecar. + DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the sidecar. `; return box; } @@ -10576,7 +10577,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running ${IMPECCABLE_COMMAND} document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/.trae-cn/skills/impeccable/scripts/live-server.mjs b/.trae-cn/skills/impeccable/scripts/live-server.mjs index 27005ef3c..5735f2aec 100644 --- a/.trae-cn/skills/impeccable/scripts/live-server.mjs +++ b/.trae-cn/skills/impeccable/scripts/live-server.mjs @@ -36,6 +36,7 @@ import { getDesignSidecarPath, getLiveDir, getLiveAnnotationsDir, + IMPECCABLE_COMMAND_PREFIX, readLiveServerInfo, removeLiveServerInfo, resolveDesignSidecarPath, @@ -413,6 +414,7 @@ function createRequestHandler({ detectScript, liveScriptParts }) { token: state.token, port: state.port, vocabulary: LIVE_COMMANDS, + commandPrefix: IMPECCABLE_COMMAND_PREFIX, parts, }); res.writeHead(200, { diff --git a/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs b/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs index 9229e34f8..b77f6a542 100644 --- a/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.trae-cn/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -32,10 +32,11 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { +export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', parts }) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + + `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; diff --git a/.trae-cn/skills/impeccable/scripts/pin.mjs b/.trae-cn/skills/impeccable/scripts/pin.mjs index b0eb39da0..52ea2701b 100644 --- a/.trae-cn/skills/impeccable/scripts/pin.mjs +++ b/.trae-cn/skills/impeccable/scripts/pin.mjs @@ -6,7 +6,7 @@ * node /pin.mjs pin * node /pin.mjs unpin * - * `pin audit` creates a lightweight /audit skill that redirects to /impeccable audit. + * `pin audit` creates a lightweight audit skill that redirects to Impeccable's audit workflow. * `unpin audit` removes that shortcut. * * The script discovers harness directories (.claude/skills, .cursor/skills, etc.) @@ -14,7 +14,7 @@ */ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs'; -import { join, resolve, dirname } from 'node:path'; +import { basename, join, resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -25,6 +25,8 @@ const HARNESS_DIRS = [ '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', ]; +const CODEX_HARNESSES = new Set(['.codex', '.agents']); + // Valid sub-command names const VALID_COMMANDS = [ 'craft', 'init', 'extract', 'document', 'shape', @@ -87,8 +89,12 @@ function loadCommandMetadata() { /** * Generate a pinned skill's SKILL.md content. */ -function generatePinnedSkill(command, metadata) { - const desc = metadata[command]?.description || `Shortcut for /impeccable ${command}.`; +function commandPrefixForSkillsDir(skillsDir) { + return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; +} + +function generatePinnedSkill(command, metadata, commandPrefix) { + const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; return `--- @@ -100,9 +106,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`/impeccable ${command}\`. +This is a pinned shortcut for \`${commandPrefix}impeccable ${command}\`. -Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } @@ -118,10 +124,11 @@ function pin(command, projectRoot) { return false; } - const content = generatePinnedSkill(command, metadata); let created = 0; for (const skillsDir of harnessDirs) { + const commandPrefix = commandPrefixForSkillsDir(skillsDir); + const content = generatePinnedSkill(command, metadata, commandPrefix); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { @@ -143,7 +150,7 @@ function pin(command, projectRoot) { if (created > 0) { console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`); - console.log(`You can now use /${command} directly.`); + console.log('Use the pinned command directly in each harness.'); } return created > 0; @@ -177,7 +184,7 @@ function unpin(command, projectRoot) { if (removed > 0) { console.log(`\nUnpinned '${command}' from ${removed} location(s).`); - console.log(`Use /impeccable ${command} to access it.`); + console.log(`Use Impeccable's '${command}' workflow directly to access it.`); } else { console.log(`No pinned '${command}' shortcut found.`); } diff --git a/.trae/skills/impeccable/scripts/context-signals.mjs b/.trae/skills/impeccable/scripts/context-signals.mjs index e12881b81..2fc27bea7 100644 --- a/.trae/skills/impeccable/scripts/context-signals.mjs +++ b/.trae/skills/impeccable/scripts/context-signals.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Context-signals gatherer for the bare `/impeccable` + * Context-signals gatherer for the bare Impeccable invocation * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.trae/skills/impeccable/scripts/context.mjs b/.trae/skills/impeccable/scripts/context.mjs index 4530f41d5..11f2aabe0 100644 --- a/.trae/skills/impeccable/scripts/context.mjs +++ b/.trae/skills/impeccable/scripts/context.mjs @@ -23,6 +23,7 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseTargetOptions } from './lib/target-args.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; @@ -902,7 +903,7 @@ async function cli() { 'or wording that clearly maps to a from-scratch build/shape flow, load ' + 'reference/init.md and write PRODUCT.md first; for any other (scoped) ' + 'command against existing code, proceed using the code as context and ' + - 'offer `/impeccable init` as a suggestion (do not block).', + `offer \`${IMPECCABLE_COMMAND} init\` as a suggestion (do not block).`, ]; parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists })); if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) { diff --git a/.trae/skills/impeccable/scripts/critique-storage.mjs b/.trae/skills/impeccable/scripts/critique-storage.mjs index 645628c8c..6b4d225cf 100644 --- a/.trae/skills/impeccable/scripts/critique-storage.mjs +++ b/.trae/skills/impeccable/scripts/critique-storage.mjs @@ -2,11 +2,11 @@ /** * Critique persistence helper. * - * Each run of /impeccable critique writes a per-target snapshot to + * Each critique run writes a per-target snapshot to * .impeccable/critique/__.md * with a small YAML frontmatter carrying the score + P0/P1 counts. * - * /impeccable polish reads the latest matching snapshot at start as its + * The polish workflow reads the latest matching snapshot at start as its * fix backlog. No other skill auto-reads critique output. * * The slug is derived mechanically from the *resolved* primary artifact diff --git a/.trae/skills/impeccable/scripts/hook-admin.mjs b/.trae/skills/impeccable/scripts/hook-admin.mjs index ce4ec7b2b..8b37b1f3b 100644 --- a/.trae/skills/impeccable/scripts/hook-admin.mjs +++ b/.trae/skills/impeccable/scripts/hook-admin.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * `/impeccable hooks ` — manage the design hook runtime + * The Impeccable hooks command manages the design hook runtime * via the `hook` key and shared detector ignores via the `detector` key in * .impeccable/config.json / .impeccable/config.local.json. * @@ -21,6 +21,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; import { getConfigPath, @@ -184,7 +185,7 @@ function writeHookConfig(cwd, hookConfig, opts = {}) { const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {}; const existingHook = stripDetectorKeys(hookSection(existing)); // Merge over the existing hook object so fields the merge helpers don't manage - // (consent, quiet, auditLog) survive a `/impeccable hooks` edit. + // (consent, quiet, auditLog) survive an Impeccable hooks edit. const next = { ...existing, hook: { ...existingHook, ...hookConfig } }; fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n'); @@ -513,9 +514,9 @@ function parseIgnoreRuleArgs(args) { function addIgnoreRule(cwd, args) { const parsed = parseIgnoreRuleArgs(args); const rule = parsed.rule; - if (!rule) throw new Error('Pass a rule id, e.g. /impeccable hooks ignore-rule side-tab'); + if (!rule) throw new Error(`Pass a rule id, e.g. ${IMPECCABLE_COMMAND} hooks ignore-rule side-tab`); if (rule === 'overused-font' && !parsed.allValues) { - throw new Error('overused-font is value-specific by default. Use /impeccable hooks ignore-value overused-font for a confirmed font, or /impeccable hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.'); + throw new Error(`overused-font is value-specific by default. Use ${IMPECCABLE_COMMAND} hooks ignore-value overused-font for a confirmed font, or ${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.`); } const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule); @@ -524,7 +525,7 @@ function addIgnoreRule(cwd, args) { } function addIgnoreFile(cwd, glob) { - if (!glob) throw new Error('Pass a glob, e.g. /impeccable hooks ignore-file "src/legacy/**"'); + if (!glob) throw new Error(`Pass a glob, e.g. ${IMPECCABLE_COMMAND} hooks ignore-file "src/legacy/**"`); const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob); writeDetectorConfig(cwd, config); @@ -569,7 +570,7 @@ function parseIgnoreValueArgs(args) { function addIgnoreValue(cwd, args) { const parsed = parseIgnoreValueArgs(args); if (!parsed.rule || !parsed.value) { - throw new Error('Pass a rule id and value, e.g. /impeccable hooks ignore-value overused-font Inter'); + throw new Error(`Pass a rule id and value, e.g. ${IMPECCABLE_COMMAND} hooks ignore-value overused-font Inter`); } if (parsed.shared && parsed.local) { diff --git a/.trae/skills/impeccable/scripts/hook-lib.mjs b/.trae/skills/impeccable/scripts/hook-lib.mjs index 4953b91ca..0d9722953 100644 --- a/.trae/skills/impeccable/scripts/hook-lib.mjs +++ b/.trae/skills/impeccable/scripts/hook-lib.mjs @@ -41,6 +41,7 @@ import os from 'node:os'; import path from 'node:path'; import { pathToFileURL, fileURLToPath } from 'node:url'; import { extractPlatform, loadContext } from './context.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -661,7 +662,7 @@ export function bumpEditCount(cache, sessionId, filePath) { } export function suppressionNotice(filePath) { - return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run /impeccable audit to revisit.`; + return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run ${IMPECCABLE_COMMAND} audit to revisit.`; } // Glob → RegExp. Supports `**`, `*`, `?`, and `{a,b}` alternation. @@ -877,7 +878,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) { const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`; const lines = shown.map((f) => formatFindingLine(f)); const more = remaining > 0 - ? `... and ${remaining} more (see /impeccable audit).` + ? `... and ${remaining} more (see ${IMPECCABLE_COMMAND} audit).` : null; const footer = directiveFooter(display); @@ -921,7 +922,7 @@ function renderGroupedTemplate(groups, config, opts = {}) { shownCount += shown.length; const hidden = group.findings.length - shown.length; if (hidden > 0) { - lines.push(`- ... ${hidden} more in ${display} (see /impeccable audit).`); + lines.push(`- ... ${hidden} more in ${display} (see ${IMPECCABLE_COMMAND} audit).`); } } @@ -937,7 +938,7 @@ function clampGroupedToBudget(header, lines, footer, maxChars) { const assemble = (linesArr, omitted) => [ header, ...linesArr, - ...(omitted ? ['... and more (see /impeccable audit).'] : []), + ...(omitted ? [`... and more (see ${IMPECCABLE_COMMAND} audit).`] : []), '', footer, ].join('\n'); @@ -970,7 +971,7 @@ function clampToBudget(header, lines, more, footer, maxChars) { let assembled = assemble(working, moreText); while (assembled.length > maxChars && working.length > 1) { working.pop(); - moreText = '... and more (see /impeccable audit).'; + moreText = `... and more (see ${IMPECCABLE_COMMAND} audit).`; assembled = assemble(working, moreText); } if (assembled.length > maxChars) { @@ -1002,7 +1003,7 @@ function formatFindingIgnoreCommand(finding) { const value = extractFindingIgnoreValueRaw(finding); const valueArg = quoteCommandArg(value); const reason = quoteCommandArg(`User confirmed ${value} is intentional`); - return `/impeccable hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; + return `${IMPECCABLE_COMMAND} hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; } function quoteCommandArg(value) { @@ -1447,7 +1448,7 @@ export function designSystemOptions(config, detector, projectCwd) { export function appendDesignSystemNote(text, scanOptions) { if (!text || !scanOptions?.designSystem?.mdNewerThanJson) return text; - return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the design-system sidecar.`; + return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the design-system sidecar.`; } // The directive footer is the part of the hook output that steers model @@ -1464,16 +1465,16 @@ export function appendDesignSystemNote(text, scanOptions) { // raw envelope. Asking the model to surface the resolution in its // reply is the cheapest way to make the feedback loop visible. function directiveFooter(display, opts = {}) { - const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`; + const ignoreFileCommand = `${IMPECCABLE_COMMAND} hooks ignore-file ${quoteCommandArg(display)}`; const fileIgnoreGuidance = opts.grouped - ? 'run `/impeccable hooks ignore-file ` for the specific file' + ? `run \`${IMPECCABLE_COMMAND} hooks ignore-file \` for the specific file` : `run \`${ignoreFileCommand}\``; return [ 'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.', '', 'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.', '', - `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`, + `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`${IMPECCABLE_COMMAND} hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`${IMPECCABLE_COMMAND} hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run ${IMPECCABLE_COMMAND} audit for the full pass.`, ].join('\n'); } diff --git a/.trae/skills/impeccable/scripts/lib/impeccable-paths.mjs b/.trae/skills/impeccable/scripts/lib/impeccable-paths.mjs index 91121dd59..2ccbe7b74 100644 --- a/.trae/skills/impeccable/scripts/lib/impeccable-paths.mjs +++ b/.trae/skills/impeccable/scripts/lib/impeccable-paths.mjs @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { resolveProjectRoot } from '../context.mjs'; +export { IMPECCABLE_COMMAND_PREFIX } from './provider.mjs'; export const IMPECCABLE_DIR = '.impeccable'; export const LIVE_DIR = 'live'; diff --git a/.trae/skills/impeccable/scripts/lib/provider.mjs b/.trae/skills/impeccable/scripts/lib/provider.mjs new file mode 100644 index 000000000..7fa928a9c --- /dev/null +++ b/.trae/skills/impeccable/scripts/lib/provider.mjs @@ -0,0 +1,4 @@ +// Source scripts default to slash commands. The provider build replaces only +// this exact declaration, avoiding heuristic rewrites across executable code. +export const IMPECCABLE_COMMAND_PREFIX = "/"; +export const IMPECCABLE_COMMAND = `${IMPECCABLE_COMMAND_PREFIX}impeccable`; diff --git a/.trae/skills/impeccable/scripts/live-browser.js b/.trae/skills/impeccable/scripts/live-browser.js index 02b8c8bcf..616f1290a 100644 --- a/.trae/skills/impeccable/scripts/live-browser.js +++ b/.trae/skills/impeccable/scripts/live-browser.js @@ -57,6 +57,7 @@ const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 }; const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint const PREFIX = 'impeccable-live'; + const IMPECCABLE_COMMAND = (window.__IMPECCABLE_COMMAND_PREFIX__ || '/') + 'impeccable'; const PICK_CURSOR_STYLE_ID = PREFIX + '-pick-cursor-style'; const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000; const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({ @@ -6145,7 +6146,7 @@ switch (msg.type) { case 'connected': hasProjectContext = !!msg.hasProjectContext; - if (!hasProjectContext) showToast('No PRODUCT.md found. Variants will be brand-agnostic. Run /impeccable init to generate one.', 7000); + if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000); console.log('[impeccable] Live mode connected.'); syncAgentPollingUi(!!msg.agentPolling); startAgentStatusPoll(); @@ -10538,7 +10539,7 @@ void main() { if (designState.present === false) { const empty = document.createElement('div'); empty.className = 'empty'; - empty.innerHTML = `No DESIGN.md yetCreate one by running /impeccable document in your terminal, then re-open this panel.`; + empty.innerHTML = `No DESIGN.md yetCreate one by running ${IMPECCABLE_COMMAND} document in your terminal, then re-open this panel.`; body.appendChild(empty); return; } @@ -10568,7 +10569,7 @@ void main() { box.className = 'stale'; box.innerHTML = ` - DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the sidecar. + DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the sidecar. `; return box; } @@ -10576,7 +10577,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running ${IMPECCABLE_COMMAND} document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/.trae/skills/impeccable/scripts/live-server.mjs b/.trae/skills/impeccable/scripts/live-server.mjs index 27005ef3c..5735f2aec 100644 --- a/.trae/skills/impeccable/scripts/live-server.mjs +++ b/.trae/skills/impeccable/scripts/live-server.mjs @@ -36,6 +36,7 @@ import { getDesignSidecarPath, getLiveDir, getLiveAnnotationsDir, + IMPECCABLE_COMMAND_PREFIX, readLiveServerInfo, removeLiveServerInfo, resolveDesignSidecarPath, @@ -413,6 +414,7 @@ function createRequestHandler({ detectScript, liveScriptParts }) { token: state.token, port: state.port, vocabulary: LIVE_COMMANDS, + commandPrefix: IMPECCABLE_COMMAND_PREFIX, parts, }); res.writeHead(200, { diff --git a/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs b/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs index 9229e34f8..b77f6a542 100644 --- a/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/.trae/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -32,10 +32,11 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { +export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', parts }) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + + `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; diff --git a/.trae/skills/impeccable/scripts/pin.mjs b/.trae/skills/impeccable/scripts/pin.mjs index b0eb39da0..52ea2701b 100644 --- a/.trae/skills/impeccable/scripts/pin.mjs +++ b/.trae/skills/impeccable/scripts/pin.mjs @@ -6,7 +6,7 @@ * node /pin.mjs pin * node /pin.mjs unpin * - * `pin audit` creates a lightweight /audit skill that redirects to /impeccable audit. + * `pin audit` creates a lightweight audit skill that redirects to Impeccable's audit workflow. * `unpin audit` removes that shortcut. * * The script discovers harness directories (.claude/skills, .cursor/skills, etc.) @@ -14,7 +14,7 @@ */ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs'; -import { join, resolve, dirname } from 'node:path'; +import { basename, join, resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -25,6 +25,8 @@ const HARNESS_DIRS = [ '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', ]; +const CODEX_HARNESSES = new Set(['.codex', '.agents']); + // Valid sub-command names const VALID_COMMANDS = [ 'craft', 'init', 'extract', 'document', 'shape', @@ -87,8 +89,12 @@ function loadCommandMetadata() { /** * Generate a pinned skill's SKILL.md content. */ -function generatePinnedSkill(command, metadata) { - const desc = metadata[command]?.description || `Shortcut for /impeccable ${command}.`; +function commandPrefixForSkillsDir(skillsDir) { + return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; +} + +function generatePinnedSkill(command, metadata, commandPrefix) { + const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; return `--- @@ -100,9 +106,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`/impeccable ${command}\`. +This is a pinned shortcut for \`${commandPrefix}impeccable ${command}\`. -Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } @@ -118,10 +124,11 @@ function pin(command, projectRoot) { return false; } - const content = generatePinnedSkill(command, metadata); let created = 0; for (const skillsDir of harnessDirs) { + const commandPrefix = commandPrefixForSkillsDir(skillsDir); + const content = generatePinnedSkill(command, metadata, commandPrefix); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { @@ -143,7 +150,7 @@ function pin(command, projectRoot) { if (created > 0) { console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`); - console.log(`You can now use /${command} directly.`); + console.log('Use the pinned command directly in each harness.'); } return created > 0; @@ -177,7 +184,7 @@ function unpin(command, projectRoot) { if (removed > 0) { console.log(`\nUnpinned '${command}' from ${removed} location(s).`); - console.log(`Use /impeccable ${command} to access it.`); + console.log(`Use Impeccable's '${command}' workflow directly to access it.`); } else { console.log(`No pinned '${command}' shortcut found.`); } diff --git a/plugin/skills/impeccable/scripts/context-signals.mjs b/plugin/skills/impeccable/scripts/context-signals.mjs index e12881b81..2fc27bea7 100644 --- a/plugin/skills/impeccable/scripts/context-signals.mjs +++ b/plugin/skills/impeccable/scripts/context-signals.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Context-signals gatherer for the bare `/impeccable` + * Context-signals gatherer for the bare Impeccable invocation * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/plugin/skills/impeccable/scripts/context.mjs b/plugin/skills/impeccable/scripts/context.mjs index 4530f41d5..11f2aabe0 100644 --- a/plugin/skills/impeccable/scripts/context.mjs +++ b/plugin/skills/impeccable/scripts/context.mjs @@ -23,6 +23,7 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseTargetOptions } from './lib/target-args.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; @@ -902,7 +903,7 @@ async function cli() { 'or wording that clearly maps to a from-scratch build/shape flow, load ' + 'reference/init.md and write PRODUCT.md first; for any other (scoped) ' + 'command against existing code, proceed using the code as context and ' + - 'offer `/impeccable init` as a suggestion (do not block).', + `offer \`${IMPECCABLE_COMMAND} init\` as a suggestion (do not block).`, ]; parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists })); if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) { diff --git a/plugin/skills/impeccable/scripts/critique-storage.mjs b/plugin/skills/impeccable/scripts/critique-storage.mjs index 645628c8c..6b4d225cf 100644 --- a/plugin/skills/impeccable/scripts/critique-storage.mjs +++ b/plugin/skills/impeccable/scripts/critique-storage.mjs @@ -2,11 +2,11 @@ /** * Critique persistence helper. * - * Each run of /impeccable critique writes a per-target snapshot to + * Each critique run writes a per-target snapshot to * .impeccable/critique/__.md * with a small YAML frontmatter carrying the score + P0/P1 counts. * - * /impeccable polish reads the latest matching snapshot at start as its + * The polish workflow reads the latest matching snapshot at start as its * fix backlog. No other skill auto-reads critique output. * * The slug is derived mechanically from the *resolved* primary artifact diff --git a/plugin/skills/impeccable/scripts/hook-admin.mjs b/plugin/skills/impeccable/scripts/hook-admin.mjs index ce4ec7b2b..8b37b1f3b 100644 --- a/plugin/skills/impeccable/scripts/hook-admin.mjs +++ b/plugin/skills/impeccable/scripts/hook-admin.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * `/impeccable hooks ` — manage the design hook runtime + * The Impeccable hooks command manages the design hook runtime * via the `hook` key and shared detector ignores via the `detector` key in * .impeccable/config.json / .impeccable/config.local.json. * @@ -21,6 +21,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; import { getConfigPath, @@ -184,7 +185,7 @@ function writeHookConfig(cwd, hookConfig, opts = {}) { const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {}; const existingHook = stripDetectorKeys(hookSection(existing)); // Merge over the existing hook object so fields the merge helpers don't manage - // (consent, quiet, auditLog) survive a `/impeccable hooks` edit. + // (consent, quiet, auditLog) survive an Impeccable hooks edit. const next = { ...existing, hook: { ...existingHook, ...hookConfig } }; fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n'); @@ -513,9 +514,9 @@ function parseIgnoreRuleArgs(args) { function addIgnoreRule(cwd, args) { const parsed = parseIgnoreRuleArgs(args); const rule = parsed.rule; - if (!rule) throw new Error('Pass a rule id, e.g. /impeccable hooks ignore-rule side-tab'); + if (!rule) throw new Error(`Pass a rule id, e.g. ${IMPECCABLE_COMMAND} hooks ignore-rule side-tab`); if (rule === 'overused-font' && !parsed.allValues) { - throw new Error('overused-font is value-specific by default. Use /impeccable hooks ignore-value overused-font for a confirmed font, or /impeccable hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.'); + throw new Error(`overused-font is value-specific by default. Use ${IMPECCABLE_COMMAND} hooks ignore-value overused-font for a confirmed font, or ${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.`); } const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule); @@ -524,7 +525,7 @@ function addIgnoreRule(cwd, args) { } function addIgnoreFile(cwd, glob) { - if (!glob) throw new Error('Pass a glob, e.g. /impeccable hooks ignore-file "src/legacy/**"'); + if (!glob) throw new Error(`Pass a glob, e.g. ${IMPECCABLE_COMMAND} hooks ignore-file "src/legacy/**"`); const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob); writeDetectorConfig(cwd, config); @@ -569,7 +570,7 @@ function parseIgnoreValueArgs(args) { function addIgnoreValue(cwd, args) { const parsed = parseIgnoreValueArgs(args); if (!parsed.rule || !parsed.value) { - throw new Error('Pass a rule id and value, e.g. /impeccable hooks ignore-value overused-font Inter'); + throw new Error(`Pass a rule id and value, e.g. ${IMPECCABLE_COMMAND} hooks ignore-value overused-font Inter`); } if (parsed.shared && parsed.local) { diff --git a/plugin/skills/impeccable/scripts/hook-lib.mjs b/plugin/skills/impeccable/scripts/hook-lib.mjs index 4953b91ca..0d9722953 100644 --- a/plugin/skills/impeccable/scripts/hook-lib.mjs +++ b/plugin/skills/impeccable/scripts/hook-lib.mjs @@ -41,6 +41,7 @@ import os from 'node:os'; import path from 'node:path'; import { pathToFileURL, fileURLToPath } from 'node:url'; import { extractPlatform, loadContext } from './context.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -661,7 +662,7 @@ export function bumpEditCount(cache, sessionId, filePath) { } export function suppressionNotice(filePath) { - return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run /impeccable audit to revisit.`; + return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run ${IMPECCABLE_COMMAND} audit to revisit.`; } // Glob → RegExp. Supports `**`, `*`, `?`, and `{a,b}` alternation. @@ -877,7 +878,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) { const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`; const lines = shown.map((f) => formatFindingLine(f)); const more = remaining > 0 - ? `... and ${remaining} more (see /impeccable audit).` + ? `... and ${remaining} more (see ${IMPECCABLE_COMMAND} audit).` : null; const footer = directiveFooter(display); @@ -921,7 +922,7 @@ function renderGroupedTemplate(groups, config, opts = {}) { shownCount += shown.length; const hidden = group.findings.length - shown.length; if (hidden > 0) { - lines.push(`- ... ${hidden} more in ${display} (see /impeccable audit).`); + lines.push(`- ... ${hidden} more in ${display} (see ${IMPECCABLE_COMMAND} audit).`); } } @@ -937,7 +938,7 @@ function clampGroupedToBudget(header, lines, footer, maxChars) { const assemble = (linesArr, omitted) => [ header, ...linesArr, - ...(omitted ? ['... and more (see /impeccable audit).'] : []), + ...(omitted ? [`... and more (see ${IMPECCABLE_COMMAND} audit).`] : []), '', footer, ].join('\n'); @@ -970,7 +971,7 @@ function clampToBudget(header, lines, more, footer, maxChars) { let assembled = assemble(working, moreText); while (assembled.length > maxChars && working.length > 1) { working.pop(); - moreText = '... and more (see /impeccable audit).'; + moreText = `... and more (see ${IMPECCABLE_COMMAND} audit).`; assembled = assemble(working, moreText); } if (assembled.length > maxChars) { @@ -1002,7 +1003,7 @@ function formatFindingIgnoreCommand(finding) { const value = extractFindingIgnoreValueRaw(finding); const valueArg = quoteCommandArg(value); const reason = quoteCommandArg(`User confirmed ${value} is intentional`); - return `/impeccable hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; + return `${IMPECCABLE_COMMAND} hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; } function quoteCommandArg(value) { @@ -1447,7 +1448,7 @@ export function designSystemOptions(config, detector, projectCwd) { export function appendDesignSystemNote(text, scanOptions) { if (!text || !scanOptions?.designSystem?.mdNewerThanJson) return text; - return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the design-system sidecar.`; + return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the design-system sidecar.`; } // The directive footer is the part of the hook output that steers model @@ -1464,16 +1465,16 @@ export function appendDesignSystemNote(text, scanOptions) { // raw envelope. Asking the model to surface the resolution in its // reply is the cheapest way to make the feedback loop visible. function directiveFooter(display, opts = {}) { - const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`; + const ignoreFileCommand = `${IMPECCABLE_COMMAND} hooks ignore-file ${quoteCommandArg(display)}`; const fileIgnoreGuidance = opts.grouped - ? 'run `/impeccable hooks ignore-file ` for the specific file' + ? `run \`${IMPECCABLE_COMMAND} hooks ignore-file \` for the specific file` : `run \`${ignoreFileCommand}\``; return [ 'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.', '', 'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.', '', - `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`, + `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`${IMPECCABLE_COMMAND} hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`${IMPECCABLE_COMMAND} hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run ${IMPECCABLE_COMMAND} audit for the full pass.`, ].join('\n'); } diff --git a/plugin/skills/impeccable/scripts/lib/impeccable-paths.mjs b/plugin/skills/impeccable/scripts/lib/impeccable-paths.mjs index 91121dd59..2ccbe7b74 100644 --- a/plugin/skills/impeccable/scripts/lib/impeccable-paths.mjs +++ b/plugin/skills/impeccable/scripts/lib/impeccable-paths.mjs @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { resolveProjectRoot } from '../context.mjs'; +export { IMPECCABLE_COMMAND_PREFIX } from './provider.mjs'; export const IMPECCABLE_DIR = '.impeccable'; export const LIVE_DIR = 'live'; diff --git a/plugin/skills/impeccable/scripts/lib/provider.mjs b/plugin/skills/impeccable/scripts/lib/provider.mjs new file mode 100644 index 000000000..7fa928a9c --- /dev/null +++ b/plugin/skills/impeccable/scripts/lib/provider.mjs @@ -0,0 +1,4 @@ +// Source scripts default to slash commands. The provider build replaces only +// this exact declaration, avoiding heuristic rewrites across executable code. +export const IMPECCABLE_COMMAND_PREFIX = "/"; +export const IMPECCABLE_COMMAND = `${IMPECCABLE_COMMAND_PREFIX}impeccable`; diff --git a/plugin/skills/impeccable/scripts/live-browser.js b/plugin/skills/impeccable/scripts/live-browser.js index 02b8c8bcf..616f1290a 100644 --- a/plugin/skills/impeccable/scripts/live-browser.js +++ b/plugin/skills/impeccable/scripts/live-browser.js @@ -57,6 +57,7 @@ const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 }; const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint const PREFIX = 'impeccable-live'; + const IMPECCABLE_COMMAND = (window.__IMPECCABLE_COMMAND_PREFIX__ || '/') + 'impeccable'; const PICK_CURSOR_STYLE_ID = PREFIX + '-pick-cursor-style'; const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000; const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({ @@ -6145,7 +6146,7 @@ switch (msg.type) { case 'connected': hasProjectContext = !!msg.hasProjectContext; - if (!hasProjectContext) showToast('No PRODUCT.md found. Variants will be brand-agnostic. Run /impeccable init to generate one.', 7000); + if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000); console.log('[impeccable] Live mode connected.'); syncAgentPollingUi(!!msg.agentPolling); startAgentStatusPoll(); @@ -10538,7 +10539,7 @@ void main() { if (designState.present === false) { const empty = document.createElement('div'); empty.className = 'empty'; - empty.innerHTML = `No DESIGN.md yetCreate one by running /impeccable document in your terminal, then re-open this panel.`; + empty.innerHTML = `No DESIGN.md yetCreate one by running ${IMPECCABLE_COMMAND} document in your terminal, then re-open this panel.`; body.appendChild(empty); return; } @@ -10568,7 +10569,7 @@ void main() { box.className = 'stale'; box.innerHTML = ` - DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the sidecar. + DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the sidecar. `; return box; } @@ -10576,7 +10577,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running ${IMPECCABLE_COMMAND} document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/plugin/skills/impeccable/scripts/live-server.mjs b/plugin/skills/impeccable/scripts/live-server.mjs index 27005ef3c..5735f2aec 100644 --- a/plugin/skills/impeccable/scripts/live-server.mjs +++ b/plugin/skills/impeccable/scripts/live-server.mjs @@ -36,6 +36,7 @@ import { getDesignSidecarPath, getLiveDir, getLiveAnnotationsDir, + IMPECCABLE_COMMAND_PREFIX, readLiveServerInfo, removeLiveServerInfo, resolveDesignSidecarPath, @@ -413,6 +414,7 @@ function createRequestHandler({ detectScript, liveScriptParts }) { token: state.token, port: state.port, vocabulary: LIVE_COMMANDS, + commandPrefix: IMPECCABLE_COMMAND_PREFIX, parts, }); res.writeHead(200, { diff --git a/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs b/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs index 9229e34f8..b77f6a542 100644 --- a/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs +++ b/plugin/skills/impeccable/scripts/live/browser-script-parts.mjs @@ -32,10 +32,11 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { +export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', parts }) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + + `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; diff --git a/plugin/skills/impeccable/scripts/pin.mjs b/plugin/skills/impeccable/scripts/pin.mjs index b0eb39da0..52ea2701b 100644 --- a/plugin/skills/impeccable/scripts/pin.mjs +++ b/plugin/skills/impeccable/scripts/pin.mjs @@ -6,7 +6,7 @@ * node /pin.mjs pin * node /pin.mjs unpin * - * `pin audit` creates a lightweight /audit skill that redirects to /impeccable audit. + * `pin audit` creates a lightweight audit skill that redirects to Impeccable's audit workflow. * `unpin audit` removes that shortcut. * * The script discovers harness directories (.claude/skills, .cursor/skills, etc.) @@ -14,7 +14,7 @@ */ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs'; -import { join, resolve, dirname } from 'node:path'; +import { basename, join, resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -25,6 +25,8 @@ const HARNESS_DIRS = [ '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', ]; +const CODEX_HARNESSES = new Set(['.codex', '.agents']); + // Valid sub-command names const VALID_COMMANDS = [ 'craft', 'init', 'extract', 'document', 'shape', @@ -87,8 +89,12 @@ function loadCommandMetadata() { /** * Generate a pinned skill's SKILL.md content. */ -function generatePinnedSkill(command, metadata) { - const desc = metadata[command]?.description || `Shortcut for /impeccable ${command}.`; +function commandPrefixForSkillsDir(skillsDir) { + return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; +} + +function generatePinnedSkill(command, metadata, commandPrefix) { + const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; return `--- @@ -100,9 +106,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`/impeccable ${command}\`. +This is a pinned shortcut for \`${commandPrefix}impeccable ${command}\`. -Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } @@ -118,10 +124,11 @@ function pin(command, projectRoot) { return false; } - const content = generatePinnedSkill(command, metadata); let created = 0; for (const skillsDir of harnessDirs) { + const commandPrefix = commandPrefixForSkillsDir(skillsDir); + const content = generatePinnedSkill(command, metadata, commandPrefix); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { @@ -143,7 +150,7 @@ function pin(command, projectRoot) { if (created > 0) { console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`); - console.log(`You can now use /${command} directly.`); + console.log('Use the pinned command directly in each harness.'); } return created > 0; @@ -177,7 +184,7 @@ function unpin(command, projectRoot) { if (removed > 0) { console.log(`\nUnpinned '${command}' from ${removed} location(s).`); - console.log(`Use /impeccable ${command} to access it.`); + console.log(`Use Impeccable's '${command}' workflow directly to access it.`); } else { console.log(`No pinned '${command}' shortcut found.`); } diff --git a/scripts/lib/codex-plugin.js b/scripts/lib/codex-plugin.js index c84c947b3..b24dd4715 100644 --- a/scripts/lib/codex-plugin.js +++ b/scripts/lib/codex-plugin.js @@ -4,7 +4,6 @@ export function buildCodexPluginManifest(rootManifest) { version: rootManifest.version, description: 'Design and refine frontend interfaces with coding agents.', author: { - ...rootManifest.author, name: 'Renaissance Geek Inc', url: rootManifest.homepage, }, diff --git a/scripts/lib/transformers/factory.js b/scripts/lib/transformers/factory.js index 12dfd819a..fafc4b06b 100644 --- a/scripts/lib/transformers/factory.js +++ b/scripts/lib/transformers/factory.js @@ -6,6 +6,7 @@ import { generateYamlFrontmatter, generateYamlDocument, replacePlaceholders, + replaceScriptProviderMarker, compileProviderBlocks, stripRuleMarkers, } from '../utils.js'; @@ -262,12 +263,7 @@ export function createTransformer(config) { const scriptsOutDir = path.join(skillDir, 'scripts'); ensureDir(scriptsOutDir); for (const script of skill.scripts) { - const scriptContent = replacePlaceholders( - script.content, - placeholderKey, - [], - allSkillNames, - ); + const scriptContent = replaceScriptProviderMarker(script.content, placeholderKey); writeFile(path.join(scriptsOutDir, script.name), scriptContent); scriptCount++; } diff --git a/scripts/lib/utils.js b/scripts/lib/utils.js index 74916c245..48a4c2c27 100644 --- a/scripts/lib/utils.js +++ b/scripts/lib/utils.js @@ -755,6 +755,22 @@ export function replacePlaceholders(content, provider, commandNames = [], allSki return result; } +/** + * Render the one explicit provider marker allowed in executable skill scripts. + * + * Do not run replacePlaceholders() across JavaScript source: slash-command + * heuristics can collide with regex literals and runtime paths. Scripts import + * their command prefix from lib/provider.mjs, whose declaration is replaced + * here by an exact string match. + */ +export function replaceScriptProviderMarker(content, provider) { + const placeholders = PROVIDER_PLACEHOLDERS[provider] || PROVIDER_PLACEHOLDERS.cursor; + const commandPrefix = placeholders.command_prefix || '/'; + const marker = "export const IMPECCABLE_COMMAND_PREFIX = '/'; // @impeccable-provider-command-prefix"; + const rendered = `export const IMPECCABLE_COMMAND_PREFIX = ${JSON.stringify(commandPrefix)};`; + return content.replace(marker, rendered); +} + /** * Decide whether a YAML scalar string value must be quoted to survive parsing. * diff --git a/scripts/test-suites.mjs b/scripts/test-suites.mjs index 6d1fb1a9d..38c6b2470 100644 --- a/scripts/test-suites.mjs +++ b/scripts/test-suites.mjs @@ -25,11 +25,11 @@ export const SUITES = { triggers: [ ...COMMON_INFRA_PATTERNS, /^scripts\/(?!benchmark-detector|build-browser-detector|build-extension)/, - /^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/(cleanup-deprecated|context|context-signals|critique-storage|design-parser|hook|impeccable-paths|is-generated))/, + /^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/(cleanup-deprecated|context|context-signals|critique-storage|design-parser|hook|impeccable-paths|is-generated|lib\/provider|pin))/, /^site\/(pages|content|components|layouts)\//, /^README(\.npm)?\.md$/, /^cli\/bin\//, - /^tests\/(build|cleanup-deprecated|cli-ignores|context|context-signals|critique-storage|design-parser|docs-integrity|github-sheriff|hook|hook-build|impeccable-paths|openai-plugin|shiki-theme|skills-cli|target-args|test-suites|windows-path-fix|zip)\.test\.(js|mjs)$/, + /^tests\/(build|cleanup-deprecated|cli-ignores|context|context-signals|critique-storage|design-parser|docs-integrity|github-sheriff|hook|hook-build|impeccable-paths|openai-plugin|pin|shiki-theme|skills-cli|target-args|test-suites|windows-path-fix|zip)\.test\.(js|mjs)$/, /^tests\/lib\//, ], commands: [ @@ -63,6 +63,7 @@ export const SUITES = { 'tests/hook.test.mjs', 'tests/impeccable-paths.test.mjs', 'tests/openai-plugin.test.mjs', + 'tests/pin.test.mjs', 'tests/target-args.test.mjs', 'tests/shiki-theme.test.mjs', 'tests/test-suites.test.mjs', diff --git a/skill/scripts/context-signals.mjs b/skill/scripts/context-signals.mjs index d82c14043..2fc27bea7 100644 --- a/skill/scripts/context-signals.mjs +++ b/skill/scripts/context-signals.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * Context-signals gatherer for the bare `{{command_prefix}}impeccable` + * Context-signals gatherer for the bare Impeccable invocation * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/skill/scripts/context.mjs b/skill/scripts/context.mjs index 4530f41d5..11f2aabe0 100644 --- a/skill/scripts/context.mjs +++ b/skill/scripts/context.mjs @@ -23,6 +23,7 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseTargetOptions } from './lib/target-args.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; @@ -902,7 +903,7 @@ async function cli() { 'or wording that clearly maps to a from-scratch build/shape flow, load ' + 'reference/init.md and write PRODUCT.md first; for any other (scoped) ' + 'command against existing code, proceed using the code as context and ' + - 'offer `/impeccable init` as a suggestion (do not block).', + `offer \`${IMPECCABLE_COMMAND} init\` as a suggestion (do not block).`, ]; parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists })); if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) { diff --git a/skill/scripts/critique-storage.mjs b/skill/scripts/critique-storage.mjs index 645628c8c..6b4d225cf 100644 --- a/skill/scripts/critique-storage.mjs +++ b/skill/scripts/critique-storage.mjs @@ -2,11 +2,11 @@ /** * Critique persistence helper. * - * Each run of /impeccable critique writes a per-target snapshot to + * Each critique run writes a per-target snapshot to * .impeccable/critique/__.md * with a small YAML frontmatter carrying the score + P0/P1 counts. * - * /impeccable polish reads the latest matching snapshot at start as its + * The polish workflow reads the latest matching snapshot at start as its * fix backlog. No other skill auto-reads critique output. * * The slug is derived mechanically from the *resolved* primary artifact diff --git a/skill/scripts/hook-admin.mjs b/skill/scripts/hook-admin.mjs index ce4ec7b2b..8b37b1f3b 100644 --- a/skill/scripts/hook-admin.mjs +++ b/skill/scripts/hook-admin.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * `/impeccable hooks ` — manage the design hook runtime + * The Impeccable hooks command manages the design hook runtime * via the `hook` key and shared detector ignores via the `detector` key in * .impeccable/config.json / .impeccable/config.local.json. * @@ -21,6 +21,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; import { getConfigPath, @@ -184,7 +185,7 @@ function writeHookConfig(cwd, hookConfig, opts = {}) { const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {}; const existingHook = stripDetectorKeys(hookSection(existing)); // Merge over the existing hook object so fields the merge helpers don't manage - // (consent, quiet, auditLog) survive a `/impeccable hooks` edit. + // (consent, quiet, auditLog) survive an Impeccable hooks edit. const next = { ...existing, hook: { ...existingHook, ...hookConfig } }; fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n'); @@ -513,9 +514,9 @@ function parseIgnoreRuleArgs(args) { function addIgnoreRule(cwd, args) { const parsed = parseIgnoreRuleArgs(args); const rule = parsed.rule; - if (!rule) throw new Error('Pass a rule id, e.g. /impeccable hooks ignore-rule side-tab'); + if (!rule) throw new Error(`Pass a rule id, e.g. ${IMPECCABLE_COMMAND} hooks ignore-rule side-tab`); if (rule === 'overused-font' && !parsed.allValues) { - throw new Error('overused-font is value-specific by default. Use /impeccable hooks ignore-value overused-font for a confirmed font, or /impeccable hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.'); + throw new Error(`overused-font is value-specific by default. Use ${IMPECCABLE_COMMAND} hooks ignore-value overused-font for a confirmed font, or ${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.`); } const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule); @@ -524,7 +525,7 @@ function addIgnoreRule(cwd, args) { } function addIgnoreFile(cwd, glob) { - if (!glob) throw new Error('Pass a glob, e.g. /impeccable hooks ignore-file "src/legacy/**"'); + if (!glob) throw new Error(`Pass a glob, e.g. ${IMPECCABLE_COMMAND} hooks ignore-file "src/legacy/**"`); const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob); writeDetectorConfig(cwd, config); @@ -569,7 +570,7 @@ function parseIgnoreValueArgs(args) { function addIgnoreValue(cwd, args) { const parsed = parseIgnoreValueArgs(args); if (!parsed.rule || !parsed.value) { - throw new Error('Pass a rule id and value, e.g. /impeccable hooks ignore-value overused-font Inter'); + throw new Error(`Pass a rule id and value, e.g. ${IMPECCABLE_COMMAND} hooks ignore-value overused-font Inter`); } if (parsed.shared && parsed.local) { diff --git a/skill/scripts/hook-lib.mjs b/skill/scripts/hook-lib.mjs index 4953b91ca..0d9722953 100644 --- a/skill/scripts/hook-lib.mjs +++ b/skill/scripts/hook-lib.mjs @@ -41,6 +41,7 @@ import os from 'node:os'; import path from 'node:path'; import { pathToFileURL, fileURLToPath } from 'node:url'; import { extractPlatform, loadContext } from './context.mjs'; +import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -661,7 +662,7 @@ export function bumpEditCount(cache, sessionId, filePath) { } export function suppressionNotice(filePath) { - return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run /impeccable audit to revisit.`; + return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run ${IMPECCABLE_COMMAND} audit to revisit.`; } // Glob → RegExp. Supports `**`, `*`, `?`, and `{a,b}` alternation. @@ -877,7 +878,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) { const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`; const lines = shown.map((f) => formatFindingLine(f)); const more = remaining > 0 - ? `... and ${remaining} more (see /impeccable audit).` + ? `... and ${remaining} more (see ${IMPECCABLE_COMMAND} audit).` : null; const footer = directiveFooter(display); @@ -921,7 +922,7 @@ function renderGroupedTemplate(groups, config, opts = {}) { shownCount += shown.length; const hidden = group.findings.length - shown.length; if (hidden > 0) { - lines.push(`- ... ${hidden} more in ${display} (see /impeccable audit).`); + lines.push(`- ... ${hidden} more in ${display} (see ${IMPECCABLE_COMMAND} audit).`); } } @@ -937,7 +938,7 @@ function clampGroupedToBudget(header, lines, footer, maxChars) { const assemble = (linesArr, omitted) => [ header, ...linesArr, - ...(omitted ? ['... and more (see /impeccable audit).'] : []), + ...(omitted ? [`... and more (see ${IMPECCABLE_COMMAND} audit).`] : []), '', footer, ].join('\n'); @@ -970,7 +971,7 @@ function clampToBudget(header, lines, more, footer, maxChars) { let assembled = assemble(working, moreText); while (assembled.length > maxChars && working.length > 1) { working.pop(); - moreText = '... and more (see /impeccable audit).'; + moreText = `... and more (see ${IMPECCABLE_COMMAND} audit).`; assembled = assemble(working, moreText); } if (assembled.length > maxChars) { @@ -1002,7 +1003,7 @@ function formatFindingIgnoreCommand(finding) { const value = extractFindingIgnoreValueRaw(finding); const valueArg = quoteCommandArg(value); const reason = quoteCommandArg(`User confirmed ${value} is intentional`); - return `/impeccable hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; + return `${IMPECCABLE_COMMAND} hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; } function quoteCommandArg(value) { @@ -1447,7 +1448,7 @@ export function designSystemOptions(config, detector, projectCwd) { export function appendDesignSystemNote(text, scanOptions) { if (!text || !scanOptions?.designSystem?.mdNewerThanJson) return text; - return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the design-system sidecar.`; + return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the design-system sidecar.`; } // The directive footer is the part of the hook output that steers model @@ -1464,16 +1465,16 @@ export function appendDesignSystemNote(text, scanOptions) { // raw envelope. Asking the model to surface the resolution in its // reply is the cheapest way to make the feedback loop visible. function directiveFooter(display, opts = {}) { - const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`; + const ignoreFileCommand = `${IMPECCABLE_COMMAND} hooks ignore-file ${quoteCommandArg(display)}`; const fileIgnoreGuidance = opts.grouped - ? 'run `/impeccable hooks ignore-file ` for the specific file' + ? `run \`${IMPECCABLE_COMMAND} hooks ignore-file \` for the specific file` : `run \`${ignoreFileCommand}\``; return [ 'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.', '', 'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.', '', - `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`, + `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable \` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`${IMPECCABLE_COMMAND} hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`${IMPECCABLE_COMMAND} hooks ignore-rule \` only when the user asks to suppress the whole non-value-specific rule. Run ${IMPECCABLE_COMMAND} audit for the full pass.`, ].join('\n'); } diff --git a/skill/scripts/lib/impeccable-paths.mjs b/skill/scripts/lib/impeccable-paths.mjs index 91121dd59..2ccbe7b74 100644 --- a/skill/scripts/lib/impeccable-paths.mjs +++ b/skill/scripts/lib/impeccable-paths.mjs @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { resolveProjectRoot } from '../context.mjs'; +export { IMPECCABLE_COMMAND_PREFIX } from './provider.mjs'; export const IMPECCABLE_DIR = '.impeccable'; export const LIVE_DIR = 'live'; diff --git a/skill/scripts/lib/provider.mjs b/skill/scripts/lib/provider.mjs new file mode 100644 index 000000000..7fd7951f5 --- /dev/null +++ b/skill/scripts/lib/provider.mjs @@ -0,0 +1,4 @@ +// Source scripts default to slash commands. The provider build replaces only +// this exact declaration, avoiding heuristic rewrites across executable code. +export const IMPECCABLE_COMMAND_PREFIX = '/'; // @impeccable-provider-command-prefix +export const IMPECCABLE_COMMAND = `${IMPECCABLE_COMMAND_PREFIX}impeccable`; diff --git a/skill/scripts/live-browser.js b/skill/scripts/live-browser.js index 02b8c8bcf..616f1290a 100644 --- a/skill/scripts/live-browser.js +++ b/skill/scripts/live-browser.js @@ -57,6 +57,7 @@ const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 }; const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint const PREFIX = 'impeccable-live'; + const IMPECCABLE_COMMAND = (window.__IMPECCABLE_COMMAND_PREFIX__ || '/') + 'impeccable'; const PICK_CURSOR_STYLE_ID = PREFIX + '-pick-cursor-style'; const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000; const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({ @@ -6145,7 +6146,7 @@ switch (msg.type) { case 'connected': hasProjectContext = !!msg.hasProjectContext; - if (!hasProjectContext) showToast('No PRODUCT.md found. Variants will be brand-agnostic. Run /impeccable init to generate one.', 7000); + if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000); console.log('[impeccable] Live mode connected.'); syncAgentPollingUi(!!msg.agentPolling); startAgentStatusPoll(); @@ -10538,7 +10539,7 @@ void main() { if (designState.present === false) { const empty = document.createElement('div'); empty.className = 'empty'; - empty.innerHTML = `No DESIGN.md yetCreate one by running /impeccable document in your terminal, then re-open this panel.`; + empty.innerHTML = `No DESIGN.md yetCreate one by running ${IMPECCABLE_COMMAND} document in your terminal, then re-open this panel.`; body.appendChild(empty); return; } @@ -10568,7 +10569,7 @@ void main() { box.className = 'stale'; box.innerHTML = ` - DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the sidecar. + DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the sidecar. `; return box; } @@ -10576,7 +10577,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running ${IMPECCABLE_COMMAND} document also generates a .impeccable/design.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/skill/scripts/live-server.mjs b/skill/scripts/live-server.mjs index 27005ef3c..5735f2aec 100644 --- a/skill/scripts/live-server.mjs +++ b/skill/scripts/live-server.mjs @@ -36,6 +36,7 @@ import { getDesignSidecarPath, getLiveDir, getLiveAnnotationsDir, + IMPECCABLE_COMMAND_PREFIX, readLiveServerInfo, removeLiveServerInfo, resolveDesignSidecarPath, @@ -413,6 +414,7 @@ function createRequestHandler({ detectScript, liveScriptParts }) { token: state.token, port: state.port, vocabulary: LIVE_COMMANDS, + commandPrefix: IMPECCABLE_COMMAND_PREFIX, parts, }); res.writeHead(200, { diff --git a/skill/scripts/live/browser-script-parts.mjs b/skill/scripts/live/browser-script-parts.mjs index 9229e34f8..b77f6a542 100644 --- a/skill/scripts/live/browser-script-parts.mjs +++ b/skill/scripts/live/browser-script-parts.mjs @@ -32,10 +32,11 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re })); } -export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) { +export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', parts }) { const prelude = `window.__IMPECCABLE_TOKEN__ = '${token}';\n` + `window.__IMPECCABLE_PORT__ = ${port};\n` + + `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` + // Canonical command vocabulary (values + labels + icons). live-browser.js // builds its action picker from this instead of an inline copy. `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`; diff --git a/skill/scripts/pin.mjs b/skill/scripts/pin.mjs index 320d98ce1..52ea2701b 100644 --- a/skill/scripts/pin.mjs +++ b/skill/scripts/pin.mjs @@ -6,7 +6,7 @@ * node /pin.mjs pin * node /pin.mjs unpin * - * `pin audit` creates a lightweight /audit skill that redirects to /impeccable audit. + * `pin audit` creates a lightweight audit skill that redirects to Impeccable's audit workflow. * `unpin audit` removes that shortcut. * * The script discovers harness directories (.claude/skills, .cursor/skills, etc.) @@ -14,7 +14,7 @@ */ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs'; -import { join, resolve, dirname } from 'node:path'; +import { basename, join, resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -25,6 +25,8 @@ const HARNESS_DIRS = [ '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', ]; +const CODEX_HARNESSES = new Set(['.codex', '.agents']); + // Valid sub-command names const VALID_COMMANDS = [ 'craft', 'init', 'extract', 'document', 'shape', @@ -87,8 +89,12 @@ function loadCommandMetadata() { /** * Generate a pinned skill's SKILL.md content. */ -function generatePinnedSkill(command, metadata) { - const desc = metadata[command]?.description || `Shortcut for /impeccable ${command}.`; +function commandPrefixForSkillsDir(skillsDir) { + return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/'; +} + +function generatePinnedSkill(command, metadata, commandPrefix) { + const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; return `--- @@ -100,9 +106,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`{{command_prefix}}impeccable ${command}\`. +This is a pinned shortcut for \`${commandPrefix}impeccable ${command}\`. -Invoke {{command_prefix}}impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } @@ -118,10 +124,11 @@ function pin(command, projectRoot) { return false; } - const content = generatePinnedSkill(command, metadata); let created = 0; for (const skillsDir of harnessDirs) { + const commandPrefix = commandPrefixForSkillsDir(skillsDir); + const content = generatePinnedSkill(command, metadata, commandPrefix); // Check if skill already exists (and isn't a pin) const skillDir = join(skillsDir, command); if (existsSync(skillDir)) { @@ -143,7 +150,7 @@ function pin(command, projectRoot) { if (created > 0) { console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`); - console.log(`You can now use /${command} directly.`); + console.log('Use the pinned command directly in each harness.'); } return created > 0; @@ -177,7 +184,7 @@ function unpin(command, projectRoot) { if (removed > 0) { console.log(`\nUnpinned '${command}' from ${removed} location(s).`); - console.log(`Use /impeccable ${command} to access it.`); + console.log(`Use Impeccable's '${command}' workflow directly to access it.`); } else { console.log(`No pinned '${command}' shortcut found.`); } diff --git a/tests/context.test.mjs b/tests/context.test.mjs index 17ddc77a9..540908d5e 100644 --- a/tests/context.test.mjs +++ b/tests/context.test.mjs @@ -925,6 +925,9 @@ describe('context.mjs update check', () => { const targetArgsDest = path.join(path.dirname(skillScript), 'lib', 'target-args.mjs'); fs.mkdirSync(path.dirname(targetArgsDest), { recursive: true }); fs.copyFileSync(targetArgsSrc, targetArgsDest); + const providerSrc = path.join(path.dirname(SCRIPT_PATH), 'lib', 'provider.mjs'); + const providerDest = path.join(path.dirname(skillScript), 'lib', 'provider.mjs'); + fs.copyFileSync(providerSrc, providerDest); fs.writeFileSync( path.join(scratch, 'skill', 'SKILL.md'), `---\nname: impeccable\nversion: ${LOCAL_VERSION}\n---\n\nbody\n`, diff --git a/tests/lib/transformers/factory.test.js b/tests/lib/transformers/factory.test.js index b3f8ee332..7f8119341 100644 --- a/tests/lib/transformers/factory.test.js +++ b/tests/lib/transformers/factory.test.js @@ -150,7 +150,7 @@ describe('createTransformer factory', () => { expect(ref1).toBe('Reference 1 content'); }); - test('should render provider command syntax in bundled scripts without rewriting paths', () => { + test('should render the explicit script provider marker without rewriting executable code', () => { const config = { ...baseConfig, provider: 'codex', @@ -164,9 +164,12 @@ describe('createTransformer factory', () => { scripts: [{ name: 'example.mjs', content: [ - 'const command = "{{command_prefix}}impeccable polish";', + "export const IMPECCABLE_COMMAND_PREFIX = '/'; // @impeccable-provider-command-prefix", + 'const command = `${IMPECCABLE_COMMAND_PREFIX}impeccable polish`;', 'const hint = "Run /impeccable audit";', 'const hook = ".github/hooks/impeccable.json";', + 'const runtime = "/src/lib/impeccable/__runtime.js";', + 'const regex = /impeccable\\b/gi;', ].join('\n'), }], }]; @@ -177,9 +180,12 @@ describe('createTransformer factory', () => { path.join(TEST_DIR, 'codex/.test/skills/impeccable/scripts/example.mjs'), 'utf-8', ); - expect(script).toContain('"$impeccable polish"'); - expect(script).toContain('"Run $impeccable audit"'); + expect(script).toContain('IMPECCABLE_COMMAND_PREFIX = "$"'); + expect(script).toContain('`${IMPECCABLE_COMMAND_PREFIX}impeccable polish`'); + expect(script).toContain('"Run /impeccable audit"'); expect(script).toContain('".github/hooks/impeccable.json"'); + expect(script).toContain('"/src/lib/impeccable/__runtime.js"'); + expect(script).toContain('/impeccable\\b/gi'); }); test('should clean existing directory before writing', () => { diff --git a/tests/lib/utils.test.js b/tests/lib/utils.test.js index 30f3c89a3..be98a0b53 100644 --- a/tests/lib/utils.test.js +++ b/tests/lib/utils.test.js @@ -10,7 +10,8 @@ import { writeFile, generateYamlFrontmatter, readPatterns, - replacePlaceholders + replacePlaceholders, + replaceScriptProviderMarker, } from '../../scripts/lib/utils.js'; // Temporary test directory @@ -673,3 +674,21 @@ describe('replacePlaceholders', () => { expect(result).toContain('https://example.com/impeccable'); }); }); + +describe('replaceScriptProviderMarker', () => { + test('renders only the explicit command-prefix declaration', () => { + const source = [ + "export const IMPECCABLE_COMMAND_PREFIX = '/'; // @impeccable-provider-command-prefix", + 'const regex = /impeccable\\b/gi;', + "const runtime = '/src/lib/impeccable/__runtime.js';", + "const text = 'Run /impeccable audit';", + ].join('\n'); + + const result = replaceScriptProviderMarker(source, 'codex'); + + expect(result).toContain('export const IMPECCABLE_COMMAND_PREFIX = "$";'); + expect(result).toContain('const regex = /impeccable\\b/gi;'); + expect(result).toContain("const runtime = '/src/lib/impeccable/__runtime.js';"); + expect(result).toContain("const text = 'Run /impeccable audit';"); + }); +}); diff --git a/tests/live-browser-script-parts.test.mjs b/tests/live-browser-script-parts.test.mjs index 808cb30e1..b8db88631 100644 --- a/tests/live-browser-script-parts.test.mjs +++ b/tests/live-browser-script-parts.test.mjs @@ -46,6 +46,7 @@ describe('live browser script parts', () => { token: 'token-a', port: 8421, vocabulary: [{ value: 'shape', label: 'Shape' }], + commandPrefix: '$', parts: [ { name: 'session-state', file: 'live-browser-session.js', source: 'window.__SESSION_PART__ = true;' }, { name: 'dom-helpers', file: 'live-browser-dom.js', source: 'window.__DOM_PART__ = true;' }, @@ -55,6 +56,7 @@ describe('live browser script parts', () => { const tokenIndex = script.indexOf('window.__IMPECCABLE_TOKEN__'); const portIndex = script.indexOf('window.__IMPECCABLE_PORT__'); + const commandPrefixIndex = script.indexOf('window.__IMPECCABLE_COMMAND_PREFIX__'); const vocabIndex = script.indexOf('window.__IMPECCABLE_VOCAB__'); const sessionIndex = script.indexOf('window.__SESSION_PART__'); const domIndex = script.indexOf('window.__DOM_PART__'); @@ -62,7 +64,9 @@ describe('live browser script parts', () => { assert.ok(tokenIndex !== -1); assert.ok(tokenIndex < portIndex); - assert.ok(portIndex < vocabIndex); + assert.ok(portIndex < commandPrefixIndex); + assert.ok(commandPrefixIndex < vocabIndex); + assert.match(script, /window\.__IMPECCABLE_COMMAND_PREFIX__ = "\$"/); assert.ok(vocabIndex < sessionIndex); assert.ok(sessionIndex < domIndex); assert.ok(domIndex < browserIndex); diff --git a/tests/openai-plugin.test.mjs b/tests/openai-plugin.test.mjs index 00736fde6..cb3b82cbd 100644 --- a/tests/openai-plugin.test.mjs +++ b/tests/openai-plugin.test.mjs @@ -80,6 +80,11 @@ describe('OpenAI plugin staging', () => { assert.ok(fs.existsSync(path.join(pluginRoot, 'assets/icon.png'))); assert.equal(manifest.skills, './skills/'); + assert.deepEqual(manifest.author, { + name: 'Renaissance Geek Inc', + url: 'https://impeccable.style', + }); + assert.equal('email' in manifest.author, false); assert.equal(manifest.interface.shortDescription, 'Design and refine interfaces'); assert.equal(manifest.interface.category, 'Creativity'); assert.deepEqual(hooks, buildCodexPluginHooksManifest()); diff --git a/tests/pin.test.mjs b/tests/pin.test.mjs new file mode 100644 index 000000000..ae58a8dec --- /dev/null +++ b/tests/pin.test.mjs @@ -0,0 +1,46 @@ +import { afterEach, beforeEach, describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; + +const ROOT = process.cwd(); +const PIN_SCRIPT = path.join(ROOT, 'skill', 'scripts', 'pin.mjs'); + +describe('pin command provider syntax', () => { + let project; + + beforeEach(() => { + project = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-pin-')); + fs.writeFileSync(path.join(project, 'package.json'), '{}\n'); + for (const harness of ['.claude', '.cursor', '.agents', '.codex']) { + fs.mkdirSync(path.join(project, harness, 'skills', 'impeccable'), { recursive: true }); + } + }); + + afterEach(() => { + fs.rmSync(project, { recursive: true, force: true }); + }); + + it('renders each pinned shortcut for its target harness', () => { + const result = spawnSync(process.execPath, [PIN_SCRIPT, 'pin', 'audit'], { + cwd: project, + encoding: 'utf8', + }); + + assert.equal(result.status, 0, result.stderr || result.stdout); + + for (const harness of ['.claude', '.cursor']) { + const skill = fs.readFileSync(path.join(project, harness, 'skills', 'audit', 'SKILL.md'), 'utf8'); + assert.match(skill, /\/impeccable audit/); + assert.doesNotMatch(skill, /\$impeccable audit/); + } + + for (const harness of ['.agents', '.codex']) { + const skill = fs.readFileSync(path.join(project, harness, 'skills', 'audit', 'SKILL.md'), 'utf8'); + assert.match(skill, /\$impeccable audit/); + assert.doesNotMatch(skill, /\/impeccable audit/); + } + }); +});