mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 15:16:35 +03:00
Add OpenAI plugin submission bundle
Build a Codex-native OpenAI plugin with bundled hooks, public listing metadata, submission guidance, privacy coverage, and regression tests. AI assistance: OpenAI Codex prepared and validated these changes under maintainer direction.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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/<timestamp>__<slug>.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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* `/impeccable hooks <on|off|status|reset>` — manage the design hook runtime
|
||||
* `$impeccable hooks <on|off|status|reset>` — 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 <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 <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) {
|
||||
|
||||
@@ -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 <path>` for the specific file'
|
||||
? 'run `$impeccable hooks ignore-file <path>` 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 <rule>\` 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 <id>\` 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 <rule>\` 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 <id>\` only when the user asks to suppress the whole non-value-specific rule. Run $impeccable audit for the full pass.`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = `<strong>No DESIGN.md yet</strong>Create one by running <code>/impeccable document</code> in your terminal, then re-open this panel.`;
|
||||
empty.innerHTML = `<strong>No DESIGN.md yet</strong>Create one by running <code>$impeccable document</code> in your terminal, then re-open this panel.`;
|
||||
body.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
@@ -10568,7 +10568,7 @@ void main() {
|
||||
box.className = 'stale';
|
||||
box.innerHTML = `
|
||||
<span class="stale-dot"></span>
|
||||
<span class="stale-text"><strong>DESIGN.md is newer than .impeccable/design.json.</strong> Run <code>/impeccable document</code> to refresh the sidecar.</span>
|
||||
<span class="stale-text"><strong>DESIGN.md is newer than .impeccable/design.json.</strong> Run <code>$impeccable document</code> to refresh the sidecar.</span>
|
||||
`;
|
||||
return box;
|
||||
}
|
||||
@@ -10576,7 +10576,7 @@ void main() {
|
||||
function renderParsedMdCta() {
|
||||
const box = document.createElement('div');
|
||||
box.className = 'parsed-md-cta';
|
||||
box.innerHTML = `<strong>Basic view</strong>This panel reads the tokens in your <code>DESIGN.md</code> frontmatter. Running <code>/impeccable document</code> also generates a <code>.impeccable/design.json</code> sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`;
|
||||
box.innerHTML = `<strong>Basic view</strong>This panel reads the tokens in your <code>DESIGN.md</code> frontmatter. Running <code>$impeccable document</code> also generates a <code>.impeccable/design.json</code> sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`;
|
||||
return box;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* node <scripts_path>/pin.mjs pin <command>
|
||||
* node <scripts_path>/pin.mjs unpin <command>
|
||||
*
|
||||
* `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.`);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
+14
-1
@@ -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);
|
||||
|
||||
|
||||
@@ -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: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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(`(?<![a-zA-Z0-9_./-])\\/(?=${escapeRegex(name)}(?:[^a-zA-Z0-9_-]|$))`, 'g'),
|
||||
cmdPrefix
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
* subtree fails loudly instead of merging a drift window onto main.
|
||||
* - `plugin/skills/impeccable/SKILL.md` frontmatter version — generated;
|
||||
* same rationale.
|
||||
* - `dist/openai/impeccable/.codex-plugin/plugin.json` version — generated
|
||||
* for public OpenAI submission and checked when that build output exists.
|
||||
*
|
||||
* The collector is pure (filesystem-in, data-out) so it can be unit-tested
|
||||
* against fixtures; build.js owns the logging and the non-zero exit.
|
||||
@@ -96,6 +98,10 @@ export function collectPluginVersions(rootDir) {
|
||||
relPath: 'plugin/.claude-plugin/plugin.json',
|
||||
read: (raw) => 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),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -33,8 +33,8 @@ import '../styles/sub-pages.css';
|
||||
<h2>Downloads</h2>
|
||||
<p>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.</p>
|
||||
|
||||
<h2>Claude Code Plugin</h2>
|
||||
<p>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.</p>
|
||||
<h2>Coding-agent plugins</h2>
|
||||
<p>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.</p>
|
||||
|
||||
<h2>Chrome Extension</h2>
|
||||
<p>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.</p>
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user