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..973b76d0c 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` * (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..c050e4a04 100644 --- a/.agents/skills/impeccable/scripts/context.mjs +++ b/.agents/skills/impeccable/scripts/context.mjs @@ -902,7 +902,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 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..41bde465b 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 run of $impeccable critique 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 + * $impeccable polish 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..5c727f514 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 + * `$impeccable hooks ` — manage the design hook runtime * via the `hook` key and shared detector ignores via the `detector` key in * .impeccable/config.json / .impeccable/config.local.json. * @@ -184,7 +184,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 a `$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 +513,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 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 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.'); } const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule); @@ -524,7 +524,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 hooks ignore-file "src/legacy/**"'); const config = mergeDetectorConfig(readRawDetectorConfig(cwd)); if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob); writeDetectorConfig(cwd, config); @@ -569,7 +569,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 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..f5cca583f 100644 --- a/.agents/skills/impeccable/scripts/hook-lib.mjs +++ b/.agents/skills/impeccable/scripts/hook-lib.mjs @@ -661,7 +661,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 audit to revisit.`; } // Glob → RegExp. Supports `**`, `*`, `?`, and `{a,b}` alternation. @@ -877,7 +877,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 audit).` : null; const footer = directiveFooter(display); @@ -921,7 +921,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 audit).`); } } @@ -937,7 +937,7 @@ function clampGroupedToBudget(header, lines, footer, maxChars) { const assemble = (linesArr, omitted) => [ header, ...linesArr, - ...(omitted ? ['... and more (see /impeccable audit).'] : []), + ...(omitted ? ['... and more (see $impeccable audit).'] : []), '', footer, ].join('\n'); @@ -970,7 +970,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 audit).'; assembled = assemble(working, moreText); } if (assembled.length > maxChars) { @@ -1002,7 +1002,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 hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`; } function quoteCommandArg(value) { @@ -1447,7 +1447,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 document to refresh the design-system sidecar.`; } // The directive footer is the part of the hook output that steers model @@ -1464,16 +1464,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 hooks ignore-file ${quoteCommandArg(display)}`; const fileIgnoreGuidance = opts.grouped - ? 'run `/impeccable hooks ignore-file ` for the specific file' + ? 'run `$impeccable 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 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.`, ].join('\n'); } diff --git a/.agents/skills/impeccable/scripts/live-browser.js b/.agents/skills/impeccable/scripts/live-browser.js index 02b8c8bcf..a9e0d5e0f 100644 --- a/.agents/skills/impeccable/scripts/live-browser.js +++ b/.agents/skills/impeccable/scripts/live-browser.js @@ -6145,7 +6145,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 init to generate one.', 7000); console.log('[impeccable] Live mode connected.'); syncAgentPollingUi(!!msg.agentPolling); startAgentStatusPoll(); @@ -10538,7 +10538,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 document in your terminal, then re-open this panel.`; body.appendChild(empty); return; } @@ -10568,7 +10568,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 document to refresh the sidecar. `; return box; } @@ -10576,7 +10576,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 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/pin.mjs b/.agents/skills/impeccable/scripts/pin.mjs index 320d98ce1..e5aaadad3 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 audit. * `unpin audit` removes that shortcut. * * The script discovers harness directories (.claude/skills, .cursor/skills, etc.) @@ -88,7 +88,7 @@ function loadCommandMetadata() { * Generate a pinned skill's SKILL.md content. */ function generatePinnedSkill(command, metadata) { - const desc = metadata[command]?.description || `Shortcut for /impeccable ${command}.`; + const desc = metadata[command]?.description || `Shortcut for $impeccable ${command}.`; const hint = metadata[command]?.argumentHint || '[target]'; return `--- @@ -100,9 +100,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`{{command_prefix}}impeccable ${command}\`. +This is a pinned shortcut for \`$impeccable ${command}\`. -Invoke {{command_prefix}}impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke $impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } @@ -177,7 +177,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 ${command} 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..e12881b81 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` * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.claude/skills/impeccable/scripts/pin.mjs b/.claude/skills/impeccable/scripts/pin.mjs index 320d98ce1..b0eb39da0 100644 --- a/.claude/skills/impeccable/scripts/pin.mjs +++ b/.claude/skills/impeccable/scripts/pin.mjs @@ -100,9 +100,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`{{command_prefix}}impeccable ${command}\`. +This is a pinned shortcut for \`/impeccable ${command}\`. -Invoke {{command_prefix}}impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } diff --git a/.cursor/skills/impeccable/scripts/context-signals.mjs b/.cursor/skills/impeccable/scripts/context-signals.mjs index d82c14043..e12881b81 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` * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.cursor/skills/impeccable/scripts/pin.mjs b/.cursor/skills/impeccable/scripts/pin.mjs index 320d98ce1..b0eb39da0 100644 --- a/.cursor/skills/impeccable/scripts/pin.mjs +++ b/.cursor/skills/impeccable/scripts/pin.mjs @@ -100,9 +100,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`{{command_prefix}}impeccable ${command}\`. +This is a pinned shortcut for \`/impeccable ${command}\`. -Invoke {{command_prefix}}impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } diff --git a/.gemini/skills/impeccable/scripts/context-signals.mjs b/.gemini/skills/impeccable/scripts/context-signals.mjs index d82c14043..e12881b81 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` * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.gemini/skills/impeccable/scripts/pin.mjs b/.gemini/skills/impeccable/scripts/pin.mjs index 320d98ce1..b0eb39da0 100644 --- a/.gemini/skills/impeccable/scripts/pin.mjs +++ b/.gemini/skills/impeccable/scripts/pin.mjs @@ -100,9 +100,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`{{command_prefix}}impeccable ${command}\`. +This is a pinned shortcut for \`/impeccable ${command}\`. -Invoke {{command_prefix}}impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } diff --git a/.github/skills/impeccable/scripts/context-signals.mjs b/.github/skills/impeccable/scripts/context-signals.mjs index d82c14043..e12881b81 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` * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.github/skills/impeccable/scripts/pin.mjs b/.github/skills/impeccable/scripts/pin.mjs index 320d98ce1..b0eb39da0 100644 --- a/.github/skills/impeccable/scripts/pin.mjs +++ b/.github/skills/impeccable/scripts/pin.mjs @@ -100,9 +100,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`{{command_prefix}}impeccable ${command}\`. +This is a pinned shortcut for \`/impeccable ${command}\`. -Invoke {{command_prefix}}impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } diff --git a/.kiro/skills/impeccable/scripts/context-signals.mjs b/.kiro/skills/impeccable/scripts/context-signals.mjs index d82c14043..e12881b81 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` * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.kiro/skills/impeccable/scripts/pin.mjs b/.kiro/skills/impeccable/scripts/pin.mjs index 320d98ce1..b0eb39da0 100644 --- a/.kiro/skills/impeccable/scripts/pin.mjs +++ b/.kiro/skills/impeccable/scripts/pin.mjs @@ -100,9 +100,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`{{command_prefix}}impeccable ${command}\`. +This is a pinned shortcut for \`/impeccable ${command}\`. -Invoke {{command_prefix}}impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } diff --git a/.opencode/skills/impeccable/scripts/context-signals.mjs b/.opencode/skills/impeccable/scripts/context-signals.mjs index d82c14043..e12881b81 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` * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.opencode/skills/impeccable/scripts/pin.mjs b/.opencode/skills/impeccable/scripts/pin.mjs index 320d98ce1..b0eb39da0 100644 --- a/.opencode/skills/impeccable/scripts/pin.mjs +++ b/.opencode/skills/impeccable/scripts/pin.mjs @@ -100,9 +100,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`{{command_prefix}}impeccable ${command}\`. +This is a pinned shortcut for \`/impeccable ${command}\`. -Invoke {{command_prefix}}impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } diff --git a/.pi/skills/impeccable/scripts/context-signals.mjs b/.pi/skills/impeccable/scripts/context-signals.mjs index d82c14043..e12881b81 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` * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.pi/skills/impeccable/scripts/pin.mjs b/.pi/skills/impeccable/scripts/pin.mjs index 320d98ce1..b0eb39da0 100644 --- a/.pi/skills/impeccable/scripts/pin.mjs +++ b/.pi/skills/impeccable/scripts/pin.mjs @@ -100,9 +100,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`{{command_prefix}}impeccable ${command}\`. +This is a pinned shortcut for \`/impeccable ${command}\`. -Invoke {{command_prefix}}impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } diff --git a/.qoder/skills/impeccable/scripts/context-signals.mjs b/.qoder/skills/impeccable/scripts/context-signals.mjs index d82c14043..e12881b81 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` * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.qoder/skills/impeccable/scripts/pin.mjs b/.qoder/skills/impeccable/scripts/pin.mjs index 320d98ce1..b0eb39da0 100644 --- a/.qoder/skills/impeccable/scripts/pin.mjs +++ b/.qoder/skills/impeccable/scripts/pin.mjs @@ -100,9 +100,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`{{command_prefix}}impeccable ${command}\`. +This is a pinned shortcut for \`/impeccable ${command}\`. -Invoke {{command_prefix}}impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } diff --git a/.rovodev/skills/impeccable/scripts/context-signals.mjs b/.rovodev/skills/impeccable/scripts/context-signals.mjs index d82c14043..e12881b81 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` * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.rovodev/skills/impeccable/scripts/pin.mjs b/.rovodev/skills/impeccable/scripts/pin.mjs index 320d98ce1..b0eb39da0 100644 --- a/.rovodev/skills/impeccable/scripts/pin.mjs +++ b/.rovodev/skills/impeccable/scripts/pin.mjs @@ -100,9 +100,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`{{command_prefix}}impeccable ${command}\`. +This is a pinned shortcut for \`/impeccable ${command}\`. -Invoke {{command_prefix}}impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } diff --git a/.trae-cn/skills/impeccable/scripts/context-signals.mjs b/.trae-cn/skills/impeccable/scripts/context-signals.mjs index d82c14043..e12881b81 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` * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.trae-cn/skills/impeccable/scripts/pin.mjs b/.trae-cn/skills/impeccable/scripts/pin.mjs index 320d98ce1..b0eb39da0 100644 --- a/.trae-cn/skills/impeccable/scripts/pin.mjs +++ b/.trae-cn/skills/impeccable/scripts/pin.mjs @@ -100,9 +100,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`{{command_prefix}}impeccable ${command}\`. +This is a pinned shortcut for \`/impeccable ${command}\`. -Invoke {{command_prefix}}impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } diff --git a/.trae/skills/impeccable/scripts/context-signals.mjs b/.trae/skills/impeccable/scripts/context-signals.mjs index d82c14043..e12881b81 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` * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/.trae/skills/impeccable/scripts/pin.mjs b/.trae/skills/impeccable/scripts/pin.mjs index 320d98ce1..b0eb39da0 100644 --- a/.trae/skills/impeccable/scripts/pin.mjs +++ b/.trae/skills/impeccable/scripts/pin.mjs @@ -100,9 +100,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`{{command_prefix}}impeccable ${command}\`. +This is a pinned shortcut for \`/impeccable ${command}\`. -Invoke {{command_prefix}}impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } 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..e12881b81 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` * (no-argument) path. Collects cheap, deterministic signals about the current * project and emits them as JSON. * diff --git a/plugin/skills/impeccable/scripts/pin.mjs b/plugin/skills/impeccable/scripts/pin.mjs index 320d98ce1..b0eb39da0 100644 --- a/plugin/skills/impeccable/scripts/pin.mjs +++ b/plugin/skills/impeccable/scripts/pin.mjs @@ -100,9 +100,9 @@ user-invocable: true ${PIN_MARKER} -This is a pinned shortcut for \`{{command_prefix}}impeccable ${command}\`. +This is a pinned shortcut for \`/impeccable ${command}\`. -Invoke {{command_prefix}}impeccable ${command}, passing along any arguments provided here, and follow its instructions. +Invoke /impeccable ${command}, passing along any arguments provided here, and follow its instructions. `; } 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..c84c947b3 --- /dev/null +++ b/scripts/lib/codex-plugin.js @@ -0,0 +1,44 @@ +export function buildCodexPluginManifest(rootManifest) { + return { + name: rootManifest.name, + version: rootManifest.version, + description: 'Design and refine frontend interfaces with coding agents.', + author: { + ...rootManifest.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..12dfd819a 100644 --- a/scripts/lib/transformers/factory.js +++ b/scripts/lib/transformers/factory.js @@ -262,7 +262,13 @@ 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 = replacePlaceholders( + script.content, + placeholderKey, + [], + allSkillNames, + ); + 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..74916c245 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..6d1fb1a9d 100644 --- a/scripts/test-suites.mjs +++ b/scripts/test-suites.mjs @@ -29,7 +29,7 @@ export const SUITES = { /^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|shiki-theme|skills-cli|target-args|test-suites|windows-path-fix|zip)\.test\.(js|mjs)$/, /^tests\/lib\//, ], commands: [ @@ -62,6 +62,7 @@ export const SUITES = { 'tests/hook-build.test.mjs', 'tests/hook.test.mjs', 'tests/impeccable-paths.test.mjs', + 'tests/openai-plugin.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/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..b3f8ee332 100644 --- a/tests/lib/transformers/factory.test.js +++ b/tests/lib/transformers/factory.test.js @@ -150,6 +150,38 @@ describe('createTransformer factory', () => { expect(ref1).toBe('Reference 1 content'); }); + test('should render provider command syntax in bundled scripts without rewriting paths', () => { + const config = { + ...baseConfig, + provider: 'codex', + placeholderProvider: 'codex', + }; + const transform = createTransformer(config); + const skills = [{ + name: 'impeccable', + description: 'Test', + body: 'Body', + scripts: [{ + name: 'example.mjs', + content: [ + 'const command = "{{command_prefix}}impeccable polish";', + 'const hint = "Run /impeccable audit";', + 'const hook = ".github/hooks/impeccable.json";', + ].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 polish"'); + expect(script).toContain('"Run $impeccable audit"'); + expect(script).toContain('".github/hooks/impeccable.json"'); + }); + 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..30f3c89a3 100644 --- a/tests/lib/utils.test.js +++ b/tests/lib/utils.test.js @@ -654,4 +654,22 @@ 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'); + }); }); diff --git a/tests/openai-plugin.test.mjs b/tests/openai-plugin.test.mjs new file mode 100644 index 000000000..00736fde6 --- /dev/null +++ b/tests/openai-plugin.test.mjs @@ -0,0 +1,89 @@ +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.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/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);