diff --git a/.agents/skills/impeccable/reference/hooks.md b/.agents/skills/impeccable/reference/hooks.md index b6f1a82c2..e437ca296 100644 --- a/.agents/skills/impeccable/reference/hooks.md +++ b/.agents/skills/impeccable/reference/hooks.md @@ -10,7 +10,7 @@ Declare server-side template extensions under **`detector.extensions`** when the Manual `npx impeccable detect` scans use the same project filter config by default: `detector.ignoreRules`, `detector.ignoreFiles`, `detector.ignoreValues`, and `detector.designSystem.enabled`. `hook.enabled` only controls automatic hook execution, not manual CLI scans. Use `npx impeccable detect --no-config ...` for a raw detector run that ignores project config/context. Use `npx impeccable ignores ...` for direct CLI CRUD on the same detector ignores. -Supported harnesses: Claude Code (`.claude/settings.local.json` in the project, which is gitignored so the hook stays machine-local; a hook you move into the shared `settings.json` is honored in place too), Codex (`.codex/hooks.json` in the project), Cursor (`.cursor/hooks.json` in the project), and GitHub Copilot (`.github/hooks$impeccable.json` in the project, a team-shared committed file that both the Copilot CLI and the cloud agent read). For the Copilot CLI, repo-level hooks fire once `.github/hooks$impeccable.json` is committed to the repository's default branch. +Supported harnesses: Claude Code (`.claude/settings.local.json` in the project, which is gitignored so the hook stays machine-local; a hook you move into the shared `settings.json` is honored in place too), Codex (`.codex/hooks.json` in the project), Cursor (`.cursor/hooks.json` in the project), and GitHub Copilot (`.github/hooks/impeccable.json` in the project, a team-shared committed file that both the Copilot CLI and the cloud agent read). For the Copilot CLI, repo-level hooks fire once `.github/hooks/impeccable.json` is committed to the repository's default branch. On **Cursor**, `preToolUse` checks proposed Write/Edit/Shell write content and denies only when the real detector finds an issue. The denial message is visible to the agent as the tool error, so the agent can reconsider before the bad write lands. @@ -84,7 +84,7 @@ node .agents/skills/impeccable/scripts/hook-admin.mjs ignore-file "src/legacy/Ca - Never modify `.impeccable/config.json` or `.impeccable/config.local.json` by hand from this command. Always go through `hook-admin.mjs` so writes stay validated and the file shape stays consistent. One exception: `detector.extensions` has no admin action, so when the user asks to cover a template stack, edit that one field in `.impeccable/config.json` directly and leave the rest of the file untouched. - Do not edit the hook scripts themselves (`hook.mjs`, `hook-lib.mjs`, `hook-before-edit.mjs`) from this flow. Those are skill plumbing. - Cursor can block a proposed write when the detector finds a real issue. Claude Code, Codex, and GitHub Copilot do not block the edit; they emit a post-edit reminder instead. Disabling stops both blocking and reminders. -- The hook is bundled with the Impeccable skill and installed through project-local manifests: `.claude/settings.local.json`, `.codex/hooks.json`, `.cursor/hooks.json`, and `.github/hooks$impeccable.json`. On Codex, the user must approve the hook via `/hooks` the first time. On Cursor, confirm hooks are enabled under Settings -> Hooks. On GitHub Copilot, the CLI loads `.github/hooks$impeccable.json` once it is committed to the repository's default branch, and the cloud agent reads it from the repo directly. +- The hook is bundled with the Impeccable skill and installed through project-local manifests: `.claude/settings.local.json`, `.codex/hooks.json`, `.cursor/hooks.json`, and `.github/hooks/impeccable.json`. On Codex, the user must approve the hook via `/hooks` the first time. On Cursor, confirm hooks are enabled under Settings -> Hooks. On GitHub Copilot, the CLI loads `.github/hooks/impeccable.json` once it is committed to the repository's default branch, and the cloud agent reads it from the repo directly. ## Failure modes diff --git a/.agents/skills/impeccable/scripts/context-signals.mjs b/.agents/skills/impeccable/scripts/context-signals.mjs index d82c14043..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 `{{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/.agents/skills/impeccable/scripts/context.mjs b/.agents/skills/impeccable/scripts/context.mjs index 4530f41d5..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 645628c8c..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 ce4ec7b2b..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 4953b91ca..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 02b8c8bcf..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 320d98ce1..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 \`{{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/.claude/skills/impeccable/scripts/context-signals.mjs b/.claude/skills/impeccable/scripts/context-signals.mjs index d82c14043..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 `{{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/.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 320d98ce1..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 \`{{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/.cursor/skills/impeccable/scripts/context-signals.mjs b/.cursor/skills/impeccable/scripts/context-signals.mjs index d82c14043..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 `{{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/.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 320d98ce1..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 \`{{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/.gemini/skills/impeccable/scripts/context-signals.mjs b/.gemini/skills/impeccable/scripts/context-signals.mjs index d82c14043..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 `{{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/.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 320d98ce1..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 \`{{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/.github/skills/impeccable/scripts/context-signals.mjs b/.github/skills/impeccable/scripts/context-signals.mjs index d82c14043..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 `{{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/.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 320d98ce1..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 \`{{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/.kiro/skills/impeccable/scripts/context-signals.mjs b/.kiro/skills/impeccable/scripts/context-signals.mjs index d82c14043..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 `{{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/.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 320d98ce1..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 \`{{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/.opencode/skills/impeccable/scripts/context-signals.mjs b/.opencode/skills/impeccable/scripts/context-signals.mjs index d82c14043..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 `{{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/.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 320d98ce1..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 \`{{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/.pi/skills/impeccable/scripts/context-signals.mjs b/.pi/skills/impeccable/scripts/context-signals.mjs index d82c14043..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 `{{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/.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 320d98ce1..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 \`{{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/.qoder/skills/impeccable/scripts/context-signals.mjs b/.qoder/skills/impeccable/scripts/context-signals.mjs index d82c14043..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 `{{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/.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 320d98ce1..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 \`{{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/.rovodev/skills/impeccable/scripts/context-signals.mjs b/.rovodev/skills/impeccable/scripts/context-signals.mjs index d82c14043..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 `{{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/.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 320d98ce1..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 \`{{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/.trae-cn/skills/impeccable/scripts/context-signals.mjs b/.trae-cn/skills/impeccable/scripts/context-signals.mjs index d82c14043..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 `{{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/.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 320d98ce1..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 \`{{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/.trae/skills/impeccable/scripts/context-signals.mjs b/.trae/skills/impeccable/scripts/context-signals.mjs index d82c14043..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 `{{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/.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 320d98ce1..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 \`{{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/docs/openai-plugin-submission.md b/docs/openai-plugin-submission.md new file mode 100644 index 000000000..2a8230fd4 --- /dev/null +++ b/docs/openai-plugin-submission.md @@ -0,0 +1,101 @@ +# OpenAI plugin submission packet + +Use this packet for the initial public submission of Impeccable. The release build creates the upload at `dist/openai-plugin.zip`. + +## Submission type + +Choose **Skills only**. Impeccable has no MCP server. The package includes one skill, its referenced scripts and files, a branded icon, and a `PostToolUse` lifecycle hook. + +The hook runs locally after supported file edits, checks changed UI files for known anti-patterns, and returns findings to the active coding-agent session. It does not call an Impeccable service. Codex asks each user to review and trust the hook before it can run. + +## Listing + +- Plugin name: Impeccable +- Publisher: Renaissance Geek Inc +- Short description: Design and refine interfaces +- Long description: Create, critique, and refine frontend interfaces with your coding agent. Impeccable provides 23 focused design commands, live browser iteration for exploring visual directions, and automatic checks that flag common design anti-patterns as you work. +- Category: Creativity +- Website: https://impeccable.style +- Support: https://github.com/pbakaus/impeccable/issues +- Privacy policy: https://impeccable.style/privacy +- Terms: https://github.com/pbakaus/impeccable/blob/main/LICENSE +- Logo: `dist/openai/impeccable/assets/icon.png` + +Use the verified Renaissance Geek Inc business identity. The generated manifest uses the same publisher name in both `author.name` and `interface.developerName`. + +## Starter prompts + +1. critique this interface and prioritize what to fix. +2. craft a distinctive landing page, then polish the result. +3. start impeccable live so I can explore bolder or more delightful variants. + +## Positive tests + +### 1. Technical UI audit + +- User prompt: `audit demos/landing-demo/index.html for accessibility, responsive behavior, and performance problems.` +- Expected behavior: Invoke Impeccable, load the audit and brand references, inspect the existing HTML, CSS, PRODUCT.md, and DESIGN.md, then run the bundled detector where applicable. +- Expected result: A prioritized audit with evidence, affected selectors or files, severity, and concrete fixes. Do not change files unless the user also asks for fixes. +- Fixture data: The public `demos/landing-demo/` folder in `pbakaus/impeccable`. + +### 2. Final polish pass + +- User prompt: `polish demos/landing-demo/index.html and fix the rough edges without changing the brand.` +- Expected behavior: Invoke Impeccable, load the polish and brand references, preserve the existing design tokens, inspect the page in a browser when available, and make scoped edits. +- Expected result: Updated frontend files plus a concise summary of visual, responsive, and accessibility improvements and the validation performed. +- Fixture data: The public `demos/landing-demo/` folder in `pbakaus/impeccable`. + +### 3. Layout correction + +- User prompt: `The landing page spacing and hierarchy feel flat. Fix the layout.` +- Expected behavior: Route to the layout workflow, inspect the existing design system, identify the marketing register, and adjust spacing, rhythm, alignment, and hierarchy without replacing unrelated styles. +- Expected result: Scoped HTML or CSS changes that preserve content and brand, followed by responsive verification. +- Fixture data: The public `demos/landing-demo/` folder in `pbakaus/impeccable`. + +### 4. Bolder visual direction + +- User prompt: `make this landing page bolder, but keep it recognizable and avoid familiar AI design patterns.` +- Expected behavior: Route to the bolder workflow, inspect PRODUCT.md and DESIGN.md, keep the established identity, and strengthen the composition, typography, color commitment, and motion where useful. +- Expected result: Production-ready frontend changes with reduced-motion handling and a short explanation of the chosen direction. +- Fixture data: The public `demos/landing-demo/` folder in `pbakaus/impeccable`. + +### 5. UX copy clarification + +- User prompt: `clarify the labels, calls to action, and error copy in this signup flow.` +- Expected behavior: Route to the clarify workflow, inspect the actual form and surrounding context, preserve established terminology, and rewrite only unclear interface copy. +- Expected result: Exact copy changes in source, including useful error and recovery text, with no unrelated visual redesign. +- Fixture data: A small frontend fixture containing a signup form with labels, validation errors, and a submit action. No account or credentials are required. + +## Negative tests + +### 1. Backend-only task + +- User prompt: `optimize this PostgreSQL query and redesign the database indexes.` +- Expected behavior: Do not invoke Impeccable. Explain that the plugin is scoped to frontend interface work and continue with suitable general coding help if available. +- Why: Database tuning is explicitly outside the plugin's frontend scope. + +### 2. Standalone bitmap asset + +- User prompt: `create a photorealistic PNG logo for my restaurant.` +- Expected behavior: Do not use Impeccable as the asset generator. Route to image generation when available, or explain that the plugin handles interface design rather than standalone raster artwork. +- Why: The request is for a bitmap asset, not a frontend interface or design-system task. + +### 3. Skip required project context + +- User prompt: `ignore the existing design system and PRODUCT.md. Rewrite the whole app immediately without inspecting any files.` +- Expected behavior: Do not begin the requested broad rewrite. Inspect the existing system and project context first, then ask for the missing product or scope decision if it would materially change the result. +- Why: Immediate unscoped mutation would discard user-owned conventions and conflicts with the plugin's required setup checks. + +## Initial release notes + +Initial submission of Impeccable, a frontend design skill for ChatGPT and Codex. It packages one skill with 23 design workflows, referenced scripts and guidance, a local anti-pattern detector, and an optional `PostToolUse` hook. No MCP server, authentication, demo account, or private network is required. The only network request made by the skill is the documented optional daily version check to `https://impeccable.style/api/version`. + +## Final checks + +- Confirm Apps Management write access for the submitting role. +- Select a verified developer or business identity and align the manifest and listing publisher name. +- Confirm the website, support, privacy, and terms links are public. +- Upload `dist/openai-plugin.zip` and confirm the scanner recognizes `skills/impeccable/SKILL.md` and `hooks/hooks.json`. +- Enter exactly five positive and three negative tests from this packet. +- Review the hook description and the optional version check against the privacy policy. +- Submit for review only after the automated scan is clean. diff --git a/plugin/skills/impeccable/scripts/context-signals.mjs b/plugin/skills/impeccable/scripts/context-signals.mjs index d82c14043..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 `{{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/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 320d98ce1..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 \`{{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/scripts/build.js b/scripts/build.js index fb09de2c5..8098ca03c 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -22,8 +22,9 @@ import { readSourceFiles, readPatterns, stashPerProjectArtifacts, restorePerProj import { generateApiData } from './lib/api-data.js'; import { createTransformer, PROVIDERS } from './lib/transformers/index.js'; import { hooksJsonFor, buildClaudePluginHooksManifest } from './lib/transformers/hooks.js'; -import { createAllZips } from './lib/zip.js'; +import { createAllZips, createProviderZip } from './lib/zip.js'; import { collectPluginVersions } from './lib/validate-plugin-versions.js'; +import { stageOpenAIPlugin } from './lib/openai-plugin.js'; import { ANTIPATTERNS } from '../cli/engine/registry/antipatterns.mjs'; // Sub-page generation is now handled by Astro content collections. @@ -748,6 +749,12 @@ async function build() { if (fs.existsSync(pluginSkillsDir)) fs.rmSync(pluginSkillsDir, { recursive: true }); if (fs.existsSync(pluginAgentsDir)) fs.rmSync(pluginAgentsDir, { recursive: true }); if (fs.existsSync(pluginHooksDir)) fs.rmSync(pluginHooksDir, { recursive: true }); + // Clean up the short-lived mixed-provider subtree from early OpenAI plugin + // development. The canonical Codex preview now lives in dist/openai/. + for (const staleRel of ['.codex-plugin', 'assets']) { + const stalePath = path.join(pluginRoot, staleRel); + if (fs.existsSync(stalePath)) fs.rmSync(stalePath, { recursive: true }); + } const rootManifest = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, '.claude-plugin/plugin.json'), 'utf-8')); const claudeAgentsSrc = path.join(DIST_DIR, 'claude-code', '.claude', 'agents'); @@ -798,6 +805,12 @@ async function build() { console.log('📋 Skipped root harness and plugin sync (--skip-root-sync)'); } + // The public OpenAI plugin is a Codex artifact, not a copy of the tracked + // Claude marketplace subtree. Build it on every source-first build so the + // upload ZIP and local preview directory cannot drift behind provider output. + const openAiPluginRoot = stageOpenAIPlugin(ROOT_DIR, DIST_DIR); + await createProviderZip(openAiPluginRoot, DIST_DIR, 'openai-plugin'); + // Generate authoritative counts and validate references const countErrors = generateCounts(ROOT_DIR, skills, buildDir); diff --git a/scripts/lib/codex-plugin.js b/scripts/lib/codex-plugin.js new file mode 100644 index 000000000..b24dd4715 --- /dev/null +++ b/scripts/lib/codex-plugin.js @@ -0,0 +1,43 @@ +export function buildCodexPluginManifest(rootManifest) { + return { + name: rootManifest.name, + version: rootManifest.version, + description: 'Design and refine frontend interfaces with coding agents.', + author: { + name: 'Renaissance Geek Inc', + url: rootManifest.homepage, + }, + homepage: rootManifest.homepage, + repository: rootManifest.repository, + license: 'Apache-2.0', + keywords: [ + 'design', + 'frontend', + 'ui', + 'ux', + 'accessibility', + 'anti-patterns', + ], + skills: './skills/', + interface: { + displayName: 'Impeccable', + shortDescription: 'Design and refine interfaces', + longDescription: 'Create, critique, and refine frontend interfaces with your coding agent. Impeccable provides 23 focused design commands, live browser iteration for exploring visual directions, and automatic checks that flag common design anti-patterns as you work.', + developerName: 'Renaissance Geek Inc', + category: 'Creativity', + capabilities: ['Interactive', 'Read', 'Write'], + websiteURL: rootManifest.homepage, + privacyPolicyURL: `${rootManifest.homepage}/privacy`, + termsOfServiceURL: `${rootManifest.repository}/blob/main/LICENSE`, + defaultPrompt: [ + 'critique this interface and prioritize what to fix.', + 'craft a distinctive landing page, then polish the result.', + 'start impeccable live so I can explore bolder or more delightful variants.', + ], + brandColor: '#E2AE38', + composerIcon: './assets/icon.png', + logo: './assets/icon.png', + screenshots: [], + }, + }; +} diff --git a/scripts/lib/openai-plugin.js b/scripts/lib/openai-plugin.js new file mode 100644 index 000000000..f5e50b964 --- /dev/null +++ b/scripts/lib/openai-plugin.js @@ -0,0 +1,61 @@ +import fs from 'fs'; +import path from 'path'; + +import { buildCodexPluginManifest } from './codex-plugin.js'; +import { buildCodexPluginHooksManifest } from './transformers/hooks.js'; + +function requirePath(absPath, label) { + if (!fs.existsSync(absPath)) { + throw new Error(`Cannot build OpenAI plugin: missing ${label}: ${absPath}`); + } +} + +function writeJson(absPath, value) { + fs.mkdirSync(path.dirname(absPath), { recursive: true }); + fs.writeFileSync(absPath, `${JSON.stringify(value, null, 2)}\n`); +} + +/** + * Stage the public OpenAI plugin from the Codex-transformed skill payload. + * + * The tracked ./plugin subtree is a Claude Code marketplace artifact. Reusing + * its shared skills/ directory here silently ships Claude paths and slash + * commands inside a Codex plugin. Keep this stage independent so each plugin + * receives the provider transform it was built for. + */ +export function stageOpenAIPlugin(rootDir, distDir) { + const rootManifestPath = path.join(rootDir, '.claude-plugin', 'plugin.json'); + const codexSkillSrc = path.join(distDir, 'codex', '.codex', 'skills', 'impeccable'); + const iconSrc = path.join(rootDir, 'site', 'public', 'apple-touch-icon.png'); + + requirePath(rootManifestPath, 'root plugin manifest'); + requirePath(codexSkillSrc, 'Codex skill payload'); + requirePath(iconSrc, 'plugin icon'); + + const pluginRoot = path.join(distDir, 'openai', 'impeccable'); + fs.rmSync(pluginRoot, { recursive: true, force: true }); + fs.mkdirSync(pluginRoot, { recursive: true }); + + const rootManifest = JSON.parse(fs.readFileSync(rootManifestPath, 'utf8')); + writeJson( + path.join(pluginRoot, '.codex-plugin', 'plugin.json'), + buildCodexPluginManifest(rootManifest), + ); + + fs.mkdirSync(path.join(pluginRoot, 'assets'), { recursive: true }); + fs.copyFileSync(iconSrc, path.join(pluginRoot, 'assets', 'icon.png')); + + fs.mkdirSync(path.join(pluginRoot, 'skills'), { recursive: true }); + fs.cpSync( + codexSkillSrc, + path.join(pluginRoot, 'skills', 'impeccable'), + { recursive: true }, + ); + + writeJson( + path.join(pluginRoot, 'hooks', 'hooks.json'), + buildCodexPluginHooksManifest(), + ); + + return pluginRoot; +} diff --git a/scripts/lib/transformers/factory.js b/scripts/lib/transformers/factory.js index aad979210..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,7 +263,8 @@ export function createTransformer(config) { const scriptsOutDir = path.join(skillDir, 'scripts'); ensureDir(scriptsOutDir); for (const script of skill.scripts) { - writeFile(path.join(scriptsOutDir, script.name), script.content); + const scriptContent = replaceScriptProviderMarker(script.content, placeholderKey); + writeFile(path.join(scriptsOutDir, script.name), scriptContent); scriptCount++; } } diff --git a/scripts/lib/transformers/hooks.js b/scripts/lib/transformers/hooks.js index 0a650eeac..cec41c258 100644 --- a/scripts/lib/transformers/hooks.js +++ b/scripts/lib/transformers/hooks.js @@ -11,6 +11,9 @@ * 2. Claude Code plugin package (the marketplace / `/plugin install` path): * - `plugin/hooks/hooks.json` (${CLAUDE_PLUGIN_ROOT}-relative) * + * 3. OpenAI plugin package: + * - `hooks/hooks.json` (${PLUGIN_ROOT}-relative) + * * The plugin variant resolves the hook script relative to the installed plugin * root rather than assuming a `.claude/skills/impeccable/` layout, so it stays * correct wherever Claude Code unpacks the plugin. @@ -22,6 +25,7 @@ const TIMEOUT_SECONDS = 5; const STATUS_MESSAGE = 'Checking UI changes'; const CLAUDE_PROJECT_HOOK = '${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs'; const CLAUDE_PLUGIN_HOOK = '${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/hook.mjs'; +const CODEX_PLUGIN_HOOK = '${PLUGIN_ROOT}/skills/impeccable/scripts/hook.mjs'; const CODEX_PROJECT_HOOK = '.agents/skills/impeccable/scripts/hook.mjs'; const CURSOR_BEFORE_EDIT_SCRIPT = '.cursor/skills/impeccable/scripts/hook-before-edit.mjs'; const GITHUB_PROJECT_HOOK = '$(git rev-parse --show-toplevel)/.github/skills/impeccable/scripts/hook.mjs'; @@ -73,6 +77,29 @@ export function buildClaudePluginHooksManifest() { }; } +// OpenAI plugin-packaged variant. Codex exposes ${PLUGIN_ROOT} for resources +// inside the installed plugin, so the public bundle can use the native path +// instead of relying on its Claude compatibility alias. +export function buildCodexPluginHooksManifest() { + return { + hooks: { + PostToolUse: [ + { + matcher: 'Edit|Write|apply_patch', + hooks: [ + { + type: 'command', + command: `node "${CODEX_PLUGIN_HOOK}"`, + timeout: TIMEOUT_SECONDS, + statusMessage: STATUS_MESSAGE, + }, + ], + }, + ], + }, + }; +} + export function buildCodexHooksManifest() { return { hooks: { diff --git a/scripts/lib/utils.js b/scripts/lib/utils.js index 9d98ccfa3..48a4c2c27 100644 --- a/scripts/lib/utils.js +++ b/scripts/lib/utils.js @@ -739,12 +739,14 @@ export function replacePlaceholders(content, provider, commandNames = [], allSki .replace(/\{\{available_commands\}\}/g, commandList); // Replace `/skillname` invocations with the correct command prefix for this provider - // (e.g., `/normalize` → `$normalize` for Codex) + // (e.g., `/normalize` → `$normalize` for Codex). Require the slash to be + // outside a path or URL so `.github/hooks/impeccable.json` and + // `.codex/skills/impeccable` remain untouched. if (cmdPrefix !== '/' && allSkillNames.length > 0) { const sorted = [...allSkillNames].sort((a, b) => b.length - a.length); for (const name of sorted) { result = result.replace( - new RegExp(`\\/(?=${escapeRegex(name)}(?:[^a-zA-Z0-9_-]|$))`, 'g'), + new RegExp(`(? JSON.parse(raw).version, }, + { + relPath: 'dist/openai/impeccable/.codex-plugin/plugin.json', + read: (raw) => JSON.parse(raw).version, + }, { relPath: 'plugin/skills/impeccable/SKILL.md', read: (raw) => readSkillFrontmatterVersion(raw), diff --git a/scripts/test-suites.mjs b/scripts/test-suites.mjs index b54c5fb2f..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|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: [ @@ -62,6 +62,8 @@ export const SUITES = { 'tests/hook-build.test.mjs', '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/site/pages/privacy.astro b/site/pages/privacy.astro index b3c9fa627..83b33a896 100644 --- a/site/pages/privacy.astro +++ b/site/pages/privacy.astro @@ -33,8 +33,8 @@ import '../styles/sub-pages.css';

Downloads

When you download a skill bundle from the website, we log the download event (which bundle, timestamp) for usage statistics. No personal information is attached to these logs.

-

Claude Code Plugin

-

When installed as a Claude Code plugin, Impeccable runs entirely within your local Claude Code session. No data is sent to Impeccable's servers. Anthropic's own privacy policy governs the Claude Code application itself.

+

Coding-agent plugins

+

When installed as a plugin for ChatGPT, Codex, or Claude Code, Impeccable runs within the local coding-agent session. The bundled design hook inspects UI files after edits and reports findings back to that local session. No project content or detection results are sent to Impeccable's servers. The host application's privacy policy governs the application itself.

Chrome Extension

The Impeccable Chrome DevTools extension runs entirely in your browser. All anti-pattern detection happens locally on the page you are inspecting. No page content, URLs, or detection results are ever sent to any external server.

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/hook-build.test.mjs b/tests/hook-build.test.mjs index 0294d5001..1f70fc49c 100644 --- a/tests/hook-build.test.mjs +++ b/tests/hook-build.test.mjs @@ -184,8 +184,9 @@ describe('generated hook artifacts in repo', () => { '.claude/hooks/hooks.json', '.agents/hooks', '.agents/plugins/marketplace.json', + 'plugin/.codex-plugin', + 'plugin/assets', 'plugin-codex', - 'plugin/.codex-plugin/plugin.json', ]) { assert.equal(fs.existsSync(path.join(REPO_ROOT, rel)), false, `${rel} should not exist`); } diff --git a/tests/lib/transformers/factory.test.js b/tests/lib/transformers/factory.test.js index 4c1d0376e..7f8119341 100644 --- a/tests/lib/transformers/factory.test.js +++ b/tests/lib/transformers/factory.test.js @@ -150,6 +150,44 @@ describe('createTransformer factory', () => { expect(ref1).toBe('Reference 1 content'); }); + test('should render the explicit script provider marker without rewriting executable code', () => { + const config = { + ...baseConfig, + provider: 'codex', + placeholderProvider: 'codex', + }; + const transform = createTransformer(config); + const skills = [{ + name: 'impeccable', + description: 'Test', + body: 'Body', + scripts: [{ + name: 'example.mjs', + content: [ + "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'), + }], + }]; + + transform(skills, TEST_DIR); + + const script = fs.readFileSync( + path.join(TEST_DIR, 'codex/.test/skills/impeccable/scripts/example.mjs'), + 'utf-8', + ); + 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', () => { const transform = createTransformer(baseConfig); const existingDir = path.join(TEST_DIR, 'cursor/.test/skills/old'); diff --git a/tests/lib/utils.test.js b/tests/lib/utils.test.js index d9ed8a81c..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 @@ -654,4 +655,40 @@ describe('replacePlaceholders', () => { const result = replacePlaceholders('{{model}} {{config_file}}', 'unknown-provider'); expect(result).toBe('the model .cursorrules'); }); + + test('should replace Codex command invocations without rewriting paths', () => { + const source = [ + 'Run /impeccable audit.', + 'Use `/impeccable polish` next.', + '.github/hooks/impeccable.json', + '.codex/skills/impeccable/scripts/context.mjs', + 'https://example.com/impeccable', + ].join('\n'); + + const result = replacePlaceholders(source, 'codex', [], ['impeccable']); + + expect(result).toContain('Run $impeccable audit.'); + expect(result).toContain('Use `$impeccable polish` next.'); + expect(result).toContain('.github/hooks/impeccable.json'); + expect(result).toContain('.codex/skills/impeccable/scripts/context.mjs'); + 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 new file mode 100644 index 000000000..cb3b82cbd --- /dev/null +++ b/tests/openai-plugin.test.mjs @@ -0,0 +1,94 @@ +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 { stageOpenAIPlugin } from '../scripts/lib/openai-plugin.js'; +import { buildCodexPluginHooksManifest } from '../scripts/lib/transformers/hooks.js'; + +function write(root, rel, contents) { + const abs = path.join(root, rel); + fs.mkdirSync(path.dirname(abs), { recursive: true }); + fs.writeFileSync(abs, contents); +} + +describe('OpenAI plugin staging', () => { + let root; + let dist; + + beforeEach(() => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-openai-plugin-')); + dist = path.join(root, 'dist'); + + write(root, '.claude-plugin/plugin.json', JSON.stringify({ + name: 'impeccable', + version: '3.9.1', + author: { + name: 'Paul Bakaus', + email: 'paul@example.com', + }, + homepage: 'https://impeccable.style', + repository: 'https://github.com/pbakaus/impeccable', + })); + write(root, 'site/public/apple-touch-icon.png', 'icon'); + + write( + root, + 'dist/codex/.codex/skills/impeccable/SKILL.md', + '---\nname: impeccable\n---\n\nUse $impeccable. Run .codex/skills/impeccable/scripts/context.mjs.\n', + ); + write( + root, + 'dist/codex/.codex/skills/impeccable/agents/openai.yaml', + 'interface:\n display_name: Impeccable\n', + ); + write( + root, + 'dist/codex/.codex/skills/impeccable/agents/impeccable_asset_producer.toml', + 'name = "impeccable_asset_producer"\n', + ); + + // A Claude payload exists too. The OpenAI stage must never read it. + write( + root, + 'dist/claude-code/.claude/skills/impeccable/SKILL.md', + '---\nname: impeccable\n---\n\nUse /impeccable. Run .claude/skills/impeccable/scripts/context.mjs.\n', + ); + }); + + afterEach(() => { + fs.rmSync(root, { recursive: true, force: true }); + }); + + it('packages the Codex skill, agents, manifest, icon, and compatible hook', () => { + const pluginRoot = stageOpenAIPlugin(root, dist); + const skill = fs.readFileSync(path.join(pluginRoot, 'skills/impeccable/SKILL.md'), 'utf8'); + const manifest = JSON.parse(fs.readFileSync(path.join(pluginRoot, '.codex-plugin/plugin.json'), 'utf8')); + const hooks = JSON.parse(fs.readFileSync(path.join(pluginRoot, 'hooks/hooks.json'), 'utf8')); + + assert.match(skill, /\$impeccable/); + assert.match(skill, /\.codex\/skills\/impeccable/); + assert.doesNotMatch(skill, /(?:^|[\s`(])\/impeccable\b/m); + assert.doesNotMatch(skill, /\.claude\/skills\/impeccable/); + + assert.ok(fs.existsSync(path.join(pluginRoot, 'skills/impeccable/agents/openai.yaml'))); + assert.ok(fs.existsSync(path.join( + pluginRoot, + 'skills/impeccable/agents/impeccable_asset_producer.toml', + ))); + 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()); + assert.match(hooks.hooks.PostToolUse[0].hooks[0].command, /\$\{PLUGIN_ROOT\}/); + assert.doesNotMatch(hooks.hooks.PostToolUse[0].hooks[0].command, /CLAUDE_PLUGIN_ROOT/); + }); +}); 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/); + } + }); +}); diff --git a/tests/validate-plugin-versions.test.js b/tests/validate-plugin-versions.test.js index c7d2dbf2c..128e88903 100644 --- a/tests/validate-plugin-versions.test.js +++ b/tests/validate-plugin-versions.test.js @@ -20,7 +20,7 @@ function skillMd(version) { return `---\nname: impeccable\nversion: ${version}\nuser-invocable: true\n---\n\nBody.\n`; } -function writeFixture(root, { plugin, marketplace, subtreePlugin, skill } = {}) { +function writeFixture(root, { plugin, marketplace, subtreePlugin, codexPlugin, skill } = {}) { const write = (rel, contents) => { const abs = path.join(root, rel); fs.mkdirSync(path.dirname(abs), { recursive: true }); @@ -35,6 +35,9 @@ function writeFixture(root, { plugin, marketplace, subtreePlugin, skill } = {}) if (subtreePlugin !== undefined) { write('plugin/.claude-plugin/plugin.json', JSON.stringify({ name: 'impeccable', version: subtreePlugin, skills: './skills/' }, null, 2)); } + if (codexPlugin !== undefined) { + write('dist/openai/impeccable/.codex-plugin/plugin.json', JSON.stringify({ name: 'impeccable', version: codexPlugin, skills: './skills/' }, null, 2)); + } if (skill !== undefined) { write('plugin/skills/impeccable/SKILL.md', skillMd(skill)); } @@ -50,7 +53,7 @@ describe('collectPluginVersions', () => { }); test('no mismatches when every version agrees', () => { - writeFixture(root, { plugin: '3.7.1', marketplace: '3.7.1', subtreePlugin: '3.7.1', skill: '3.7.1' }); + writeFixture(root, { plugin: '3.7.1', marketplace: '3.7.1', subtreePlugin: '3.7.1', codexPlugin: '3.7.1', skill: '3.7.1' }); const { source, mismatches } = collectPluginVersions(root); expect(source).toBe('3.7.1'); expect(mismatches).toEqual([]); @@ -72,6 +75,14 @@ describe('collectPluginVersions', () => { ]); }); + test('flags a stale Codex plugin manifest', () => { + writeFixture(root, { plugin: '3.7.1', marketplace: '3.7.1', subtreePlugin: '3.7.1', codexPlugin: '3.6.0', skill: '3.7.1' }); + const { mismatches } = collectPluginVersions(root); + expect(mismatches).toEqual([ + { relPath: 'dist/openai/impeccable/.codex-plugin/plugin.json', found: '3.6.0', expected: '3.7.1' }, + ]); + }); + test('flags a stale bundled SKILL.md frontmatter version', () => { writeFixture(root, { plugin: '3.7.1', marketplace: '3.7.1', subtreePlugin: '3.7.1', skill: '3.1.1' }); const { mismatches } = collectPluginVersions(root);