From ddc372427fbed3d3df87fa4047c3e96492c15eb7 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Thu, 9 Jul 2026 17:05:38 -0700 Subject: [PATCH] Fix provider script command rendering Replace heuristic rewrites across executable scripts with one explicit provider marker, render pinned shortcuts per target harness, and remove the personal email from the public publisher manifest. Addresses automated review feedback on PR #363. AI assistance: OpenAI Codex prepared and validated these changes under maintainer direction. --- .../impeccable/scripts/context-signals.mjs | 2 +- .agents/skills/impeccable/scripts/context.mjs | 3 +- .../impeccable/scripts/critique-storage.mjs | 4 +- .../skills/impeccable/scripts/hook-admin.mjs | 13 +++--- .../skills/impeccable/scripts/hook-lib.mjs | 21 +++++---- .../scripts/lib/impeccable-paths.mjs | 1 + .../impeccable/scripts/lib/provider.mjs | 4 ++ .../skills/impeccable/scripts/live-browser.js | 9 ++-- .../skills/impeccable/scripts/live-server.mjs | 2 + .../scripts/live/browser-script-parts.mjs | 3 +- .agents/skills/impeccable/scripts/pin.mjs | 25 ++++++---- .../impeccable/scripts/context-signals.mjs | 2 +- .claude/skills/impeccable/scripts/context.mjs | 3 +- .../impeccable/scripts/critique-storage.mjs | 4 +- .../skills/impeccable/scripts/hook-admin.mjs | 13 +++--- .../skills/impeccable/scripts/hook-lib.mjs | 21 +++++---- .../scripts/lib/impeccable-paths.mjs | 1 + .../impeccable/scripts/lib/provider.mjs | 4 ++ .../skills/impeccable/scripts/live-browser.js | 9 ++-- .../skills/impeccable/scripts/live-server.mjs | 2 + .../scripts/live/browser-script-parts.mjs | 3 +- .claude/skills/impeccable/scripts/pin.mjs | 25 ++++++---- .../impeccable/scripts/context-signals.mjs | 2 +- .cursor/skills/impeccable/scripts/context.mjs | 3 +- .../impeccable/scripts/critique-storage.mjs | 4 +- .../skills/impeccable/scripts/hook-admin.mjs | 13 +++--- .../skills/impeccable/scripts/hook-lib.mjs | 21 +++++---- .../scripts/lib/impeccable-paths.mjs | 1 + .../impeccable/scripts/lib/provider.mjs | 4 ++ .../skills/impeccable/scripts/live-browser.js | 9 ++-- .../skills/impeccable/scripts/live-server.mjs | 2 + .../scripts/live/browser-script-parts.mjs | 3 +- .cursor/skills/impeccable/scripts/pin.mjs | 25 ++++++---- .../impeccable/scripts/context-signals.mjs | 2 +- .gemini/skills/impeccable/scripts/context.mjs | 3 +- .../impeccable/scripts/critique-storage.mjs | 4 +- .../skills/impeccable/scripts/hook-admin.mjs | 13 +++--- .../skills/impeccable/scripts/hook-lib.mjs | 21 +++++---- .../scripts/lib/impeccable-paths.mjs | 1 + .../impeccable/scripts/lib/provider.mjs | 4 ++ .../skills/impeccable/scripts/live-browser.js | 9 ++-- .../skills/impeccable/scripts/live-server.mjs | 2 + .../scripts/live/browser-script-parts.mjs | 3 +- .gemini/skills/impeccable/scripts/pin.mjs | 25 ++++++---- .../impeccable/scripts/context-signals.mjs | 2 +- .github/skills/impeccable/scripts/context.mjs | 3 +- .../impeccable/scripts/critique-storage.mjs | 4 +- .../skills/impeccable/scripts/hook-admin.mjs | 13 +++--- .../skills/impeccable/scripts/hook-lib.mjs | 21 +++++---- .../scripts/lib/impeccable-paths.mjs | 1 + .../impeccable/scripts/lib/provider.mjs | 4 ++ .../skills/impeccable/scripts/live-browser.js | 9 ++-- .../skills/impeccable/scripts/live-server.mjs | 2 + .../scripts/live/browser-script-parts.mjs | 3 +- .github/skills/impeccable/scripts/pin.mjs | 25 ++++++---- .../impeccable/scripts/context-signals.mjs | 2 +- .kiro/skills/impeccable/scripts/context.mjs | 3 +- .../impeccable/scripts/critique-storage.mjs | 4 +- .../skills/impeccable/scripts/hook-admin.mjs | 13 +++--- .kiro/skills/impeccable/scripts/hook-lib.mjs | 21 +++++---- .../scripts/lib/impeccable-paths.mjs | 1 + .../impeccable/scripts/lib/provider.mjs | 4 ++ .../skills/impeccable/scripts/live-browser.js | 9 ++-- .../skills/impeccable/scripts/live-server.mjs | 2 + .../scripts/live/browser-script-parts.mjs | 3 +- .kiro/skills/impeccable/scripts/pin.mjs | 25 ++++++---- .../impeccable/scripts/context-signals.mjs | 2 +- .../skills/impeccable/scripts/context.mjs | 3 +- .../impeccable/scripts/critique-storage.mjs | 4 +- .../skills/impeccable/scripts/hook-admin.mjs | 13 +++--- .../skills/impeccable/scripts/hook-lib.mjs | 21 +++++---- .../scripts/lib/impeccable-paths.mjs | 1 + .../impeccable/scripts/lib/provider.mjs | 4 ++ .../skills/impeccable/scripts/live-browser.js | 9 ++-- .../skills/impeccable/scripts/live-server.mjs | 2 + .../scripts/live/browser-script-parts.mjs | 3 +- .opencode/skills/impeccable/scripts/pin.mjs | 25 ++++++---- .../impeccable/scripts/context-signals.mjs | 2 +- .pi/skills/impeccable/scripts/context.mjs | 3 +- .../impeccable/scripts/critique-storage.mjs | 4 +- .pi/skills/impeccable/scripts/hook-admin.mjs | 13 +++--- .pi/skills/impeccable/scripts/hook-lib.mjs | 21 +++++---- .../scripts/lib/impeccable-paths.mjs | 1 + .../impeccable/scripts/lib/provider.mjs | 4 ++ .pi/skills/impeccable/scripts/live-browser.js | 9 ++-- .pi/skills/impeccable/scripts/live-server.mjs | 2 + .../scripts/live/browser-script-parts.mjs | 3 +- .pi/skills/impeccable/scripts/pin.mjs | 25 ++++++---- .../impeccable/scripts/context-signals.mjs | 2 +- .qoder/skills/impeccable/scripts/context.mjs | 3 +- .../impeccable/scripts/critique-storage.mjs | 4 +- .../skills/impeccable/scripts/hook-admin.mjs | 13 +++--- .qoder/skills/impeccable/scripts/hook-lib.mjs | 21 +++++---- .../scripts/lib/impeccable-paths.mjs | 1 + .../impeccable/scripts/lib/provider.mjs | 4 ++ .../skills/impeccable/scripts/live-browser.js | 9 ++-- .../skills/impeccable/scripts/live-server.mjs | 2 + .../scripts/live/browser-script-parts.mjs | 3 +- .qoder/skills/impeccable/scripts/pin.mjs | 25 ++++++---- .../impeccable/scripts/context-signals.mjs | 2 +- .../skills/impeccable/scripts/context.mjs | 3 +- .../impeccable/scripts/critique-storage.mjs | 4 +- .../skills/impeccable/scripts/hook-admin.mjs | 13 +++--- .../skills/impeccable/scripts/hook-lib.mjs | 21 +++++---- .../scripts/lib/impeccable-paths.mjs | 1 + .../impeccable/scripts/lib/provider.mjs | 4 ++ .../skills/impeccable/scripts/live-browser.js | 9 ++-- .../skills/impeccable/scripts/live-server.mjs | 2 + .../scripts/live/browser-script-parts.mjs | 3 +- .rovodev/skills/impeccable/scripts/pin.mjs | 25 ++++++---- .../impeccable/scripts/context-signals.mjs | 2 +- .../skills/impeccable/scripts/context.mjs | 3 +- .../impeccable/scripts/critique-storage.mjs | 4 +- .../skills/impeccable/scripts/hook-admin.mjs | 13 +++--- .../skills/impeccable/scripts/hook-lib.mjs | 21 +++++---- .../scripts/lib/impeccable-paths.mjs | 1 + .../impeccable/scripts/lib/provider.mjs | 4 ++ .../skills/impeccable/scripts/live-browser.js | 9 ++-- .../skills/impeccable/scripts/live-server.mjs | 2 + .../scripts/live/browser-script-parts.mjs | 3 +- .trae-cn/skills/impeccable/scripts/pin.mjs | 25 ++++++---- .../impeccable/scripts/context-signals.mjs | 2 +- .trae/skills/impeccable/scripts/context.mjs | 3 +- .../impeccable/scripts/critique-storage.mjs | 4 +- .../skills/impeccable/scripts/hook-admin.mjs | 13 +++--- .trae/skills/impeccable/scripts/hook-lib.mjs | 21 +++++---- .../scripts/lib/impeccable-paths.mjs | 1 + .../impeccable/scripts/lib/provider.mjs | 4 ++ .../skills/impeccable/scripts/live-browser.js | 9 ++-- .../skills/impeccable/scripts/live-server.mjs | 2 + .../scripts/live/browser-script-parts.mjs | 3 +- .trae/skills/impeccable/scripts/pin.mjs | 25 ++++++---- .../impeccable/scripts/context-signals.mjs | 2 +- plugin/skills/impeccable/scripts/context.mjs | 3 +- .../impeccable/scripts/critique-storage.mjs | 4 +- .../skills/impeccable/scripts/hook-admin.mjs | 13 +++--- plugin/skills/impeccable/scripts/hook-lib.mjs | 21 +++++---- .../scripts/lib/impeccable-paths.mjs | 1 + .../impeccable/scripts/lib/provider.mjs | 4 ++ .../skills/impeccable/scripts/live-browser.js | 9 ++-- .../skills/impeccable/scripts/live-server.mjs | 2 + .../scripts/live/browser-script-parts.mjs | 3 +- plugin/skills/impeccable/scripts/pin.mjs | 25 ++++++---- scripts/lib/codex-plugin.js | 1 - scripts/lib/transformers/factory.js | 8 +--- scripts/lib/utils.js | 16 +++++++ scripts/test-suites.mjs | 5 +- skill/scripts/context-signals.mjs | 2 +- skill/scripts/context.mjs | 3 +- skill/scripts/critique-storage.mjs | 4 +- skill/scripts/hook-admin.mjs | 13 +++--- skill/scripts/hook-lib.mjs | 21 +++++---- skill/scripts/lib/impeccable-paths.mjs | 1 + skill/scripts/lib/provider.mjs | 4 ++ skill/scripts/live-browser.js | 9 ++-- skill/scripts/live-server.mjs | 2 + skill/scripts/live/browser-script-parts.mjs | 3 +- skill/scripts/pin.mjs | 25 ++++++---- tests/context.test.mjs | 3 ++ tests/lib/transformers/factory.test.js | 14 ++++-- tests/lib/utils.test.js | 21 ++++++++- tests/live-browser-script-parts.test.mjs | 6 ++- tests/openai-plugin.test.mjs | 5 ++ tests/pin.test.mjs | 46 +++++++++++++++++++ 164 files changed, 852 insertions(+), 491 deletions(-) create mode 100644 .agents/skills/impeccable/scripts/lib/provider.mjs create mode 100644 .claude/skills/impeccable/scripts/lib/provider.mjs create mode 100644 .cursor/skills/impeccable/scripts/lib/provider.mjs create mode 100644 .gemini/skills/impeccable/scripts/lib/provider.mjs create mode 100644 .github/skills/impeccable/scripts/lib/provider.mjs create mode 100644 .kiro/skills/impeccable/scripts/lib/provider.mjs create mode 100644 .opencode/skills/impeccable/scripts/lib/provider.mjs create mode 100644 .pi/skills/impeccable/scripts/lib/provider.mjs create mode 100644 .qoder/skills/impeccable/scripts/lib/provider.mjs create mode 100644 .rovodev/skills/impeccable/scripts/lib/provider.mjs create mode 100644 .trae-cn/skills/impeccable/scripts/lib/provider.mjs create mode 100644 .trae/skills/impeccable/scripts/lib/provider.mjs create mode 100644 plugin/skills/impeccable/scripts/lib/provider.mjs create mode 100644 skill/scripts/lib/provider.mjs create mode 100644 tests/pin.test.mjs 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/); + } + }); +});