mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Give plugin agent files the plugin-root variable, not the skill token
A spawned agent never loads SKILL.md, so the <skill-base-dir> token
Setup defines is undefined in the one context that must act on it
(review finding). Claude Code substitutes ${CLAUDE_PLUGIN_ROOT} inline
anywhere in plugin agent content, so the agents directory now gets its
own rewrite emitting the quoted variable form; the skill and reference
files keep the token, which the main thread's base-directory report
resolves.
Drafted with AI assistance, reviewed by a maintainer.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -56,7 +56,7 @@ Ask blockers once, globally. Missing source path/crops or output directory block
|
||||
|
||||
7. Remove baked-in UI text, navigation, buttons, body copy, and mock chrome unless the text is part of the asset.
|
||||
8. Think through the final DOM/CSS representation before generating. If CSS will own radius, clipping, shadows, borders, perspective, responsive cropping, captions, or card frames, do not bake those into the bitmap.
|
||||
9. Save outputs non-destructively in the requested project directory, and leave the intent with the file: after every generation, run `node "<skill-base-dir>/scripts/embed-prompt.mjs" <asset> --prompt "<the prompt used>"` so the prompt lives inside the image itself. The build thread composes what you made and needs to know what it is looking at, and the embedding survives copies where sidecars get lost.
|
||||
9. Save outputs non-destructively in the requested project directory, and leave the intent with the file: after every generation, run `node "${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/embed-prompt.mjs" <asset> --prompt "<the prompt used>"` so the prompt lives inside the image itself. The build thread composes what you made and needs to know what it is looking at, and the embedding survives copies where sidecars get lost.
|
||||
10. Compare each output against its source crop, opening every image by its workspace-relative path; sandboxed viewers reject absolute paths. If a review/QA tool is available, run it before the final manifest, then retry each major/fatal finding once before finalizing.
|
||||
|
||||
Use `texture/pattern extraction` only when the source region is already clean enough to sample as texture. If UI, cards, labels, headings, body copy, or footer chrome must be removed first, classify it as crop-derived cleanup or clean-plate work.
|
||||
|
||||
+8
-2
@@ -24,7 +24,11 @@ import { hooksJsonFor, buildClaudePluginHooksManifest } from './lib/transformers
|
||||
import { createAllZips, createProviderZip } from './lib/zip.js';
|
||||
import { collectPluginVersions } from './lib/validate-plugin-versions.js';
|
||||
import { collectPluginManifestFindings } from './lib/validate-plugin-manifest.js';
|
||||
import { rewritePluginMarkdownTree, verifyPluginSkillRewrite } from './lib/plugin-paths.js';
|
||||
import {
|
||||
rewritePluginMarkdownTree,
|
||||
rewritePluginAgentMarkdown,
|
||||
verifyPluginSkillRewrite,
|
||||
} from './lib/plugin-paths.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.
|
||||
@@ -755,7 +759,9 @@ async function build() {
|
||||
// so a dual install silently runs the project's older skill copy (issue
|
||||
// #523). Rewrite the copied markdown to the skill-base-dir form.
|
||||
rewritePluginMarkdownTree(pluginSkillsDir);
|
||||
rewritePluginMarkdownTree(pluginAgentsDir);
|
||||
// Agents get the plugin-root variable, not the skill-base-dir token:
|
||||
// a spawned agent never loads SKILL.md, so the token is undefined there.
|
||||
rewritePluginMarkdownTree(pluginAgentsDir, rewritePluginAgentMarkdown);
|
||||
verifyPluginSkillRewrite(path.join(pluginSkillsDir, 'impeccable', 'SKILL.md'));
|
||||
|
||||
// Ship the design detector as a plugin-packaged hook. Claude Code and
|
||||
|
||||
@@ -44,6 +44,15 @@ const SETUP_FALLBACK_TEXT =
|
||||
const SETUP_PLUGIN_TEXT =
|
||||
'Every `node "<skill-base-dir>/scripts/..."` command in this skill and its references resolves against that base directory.';
|
||||
|
||||
// Agent files are subagent system prompts: a spawned agent never loads
|
||||
// SKILL.md, so Setup's <skill-base-dir> token is undefined in the one
|
||||
// context that must act on it (review finding). Claude Code substitutes
|
||||
// ${CLAUDE_PLUGIN_ROOT} inline anywhere in plugin skill and agent content
|
||||
// (code.claude.com/docs/en/plugins-reference), so agent instructions carry
|
||||
// the variable form; where a harness leaves it unsubstituted, the variable
|
||||
// still names the plugin install directory for the agent to locate.
|
||||
export const PLUGIN_AGENT_SCRIPTS_PATH = '${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts';
|
||||
|
||||
/**
|
||||
* Rewrite one markdown file's content for the plugin subtree. Pure, so the
|
||||
* unit suite can pin every rewrite without a build.
|
||||
@@ -64,6 +73,21 @@ export function rewritePluginMarkdown(content) {
|
||||
.replace(/node <skill-base-dir>\/scripts\/([^\s`"]+)/g, 'node "<skill-base-dir>/scripts/$1"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrite one agent file's content for the plugin subtree. Same quoting
|
||||
* discipline as the skill rewrite, but the path is the plugin-root
|
||||
* variable rather than the skill-base-dir token SKILL.md defines,
|
||||
* because no SKILL.md travels with a spawned agent.
|
||||
*/
|
||||
export function rewritePluginAgentMarkdown(content) {
|
||||
return content
|
||||
.replaceAll(CLAUDE_PROJECT_SCRIPTS_PATH, PLUGIN_AGENT_SCRIPTS_PATH)
|
||||
.replace(
|
||||
/node \$\{CLAUDE_PLUGIN_ROOT\}\/skills\/impeccable\/scripts\/([^\s`"]+)/g,
|
||||
'node "${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/$1"',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail the build when the copied SKILL.md no longer matches the rewrite.
|
||||
* The fallback-sentence replacement keys on the exact Setup step 1 text; if
|
||||
@@ -98,20 +122,21 @@ export function verifyPluginSkillRewrite(skillMdPath) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply rewritePluginMarkdown to every .md file under dir, recursively.
|
||||
* Apply a rewrite to every .md file under dir, recursively. Defaults to
|
||||
* the skill rewrite; the agents directory passes rewritePluginAgentMarkdown.
|
||||
* Script files are left alone: the only project-relative paths in them
|
||||
* (hook-admin.mjs) install project-scoped hooks via ${CLAUDE_PROJECT_DIR},
|
||||
* which is that command's actual job.
|
||||
*/
|
||||
export function rewritePluginMarkdownTree(dir) {
|
||||
export function rewritePluginMarkdownTree(dir, rewrite = rewritePluginMarkdown) {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const entryPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
rewritePluginMarkdownTree(entryPath);
|
||||
rewritePluginMarkdownTree(entryPath, rewrite);
|
||||
} else if (entry.name.endsWith('.md')) {
|
||||
const original = fs.readFileSync(entryPath, 'utf-8');
|
||||
const rewritten = rewritePluginMarkdown(original);
|
||||
const rewritten = rewrite(original);
|
||||
if (rewritten !== original) fs.writeFileSync(entryPath, rewritten);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import os from 'os';
|
||||
import path from 'path';
|
||||
import {
|
||||
rewritePluginMarkdown,
|
||||
rewritePluginAgentMarkdown,
|
||||
rewritePluginMarkdownTree,
|
||||
verifyPluginSkillRewrite,
|
||||
CLAUDE_PROJECT_SCRIPTS_PATH,
|
||||
@@ -92,6 +93,26 @@ describe('rewritePluginMarkdown', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('rewritePluginAgentMarkdown', () => {
|
||||
test('rewrites agent instructions to the quoted plugin-root variable form', () => {
|
||||
// A spawned agent never loads SKILL.md, so the <skill-base-dir> token
|
||||
// Setup defines is unresolvable in its prompt. Claude Code substitutes
|
||||
// ${CLAUDE_PLUGIN_ROOT} inline in plugin agent content.
|
||||
const input =
|
||||
'run `node .claude/skills/impeccable/scripts/embed-prompt.mjs <asset> --prompt "<the prompt used>"`';
|
||||
expect(rewritePluginAgentMarkdown(input)).toBe(
|
||||
'run `node "${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/embed-prompt.mjs" <asset> --prompt "<the prompt used>"`',
|
||||
);
|
||||
});
|
||||
|
||||
test('never emits the skill-base-dir token into an agent file', () => {
|
||||
const input = 'node .claude/skills/impeccable/scripts/embed-prompt.mjs asset.png';
|
||||
const output = rewritePluginAgentMarkdown(input);
|
||||
expect(output).not.toContain('<skill-base-dir>');
|
||||
expect(output).not.toContain(CLAUDE_PROJECT_SCRIPTS_PATH);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rewritePluginMarkdownTree', () => {
|
||||
let root;
|
||||
|
||||
@@ -130,6 +151,21 @@ describe('rewritePluginMarkdownTree', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('applies the agent rewrite when passed for an agents tree', () => {
|
||||
const agentsDir = path.join(root, 'agents');
|
||||
fs.mkdirSync(agentsDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(agentsDir, 'impeccable-asset-producer.md'),
|
||||
'run `node .claude/skills/impeccable/scripts/embed-prompt.mjs <asset>`',
|
||||
);
|
||||
|
||||
rewritePluginMarkdownTree(agentsDir, rewritePluginAgentMarkdown);
|
||||
|
||||
expect(fs.readFileSync(path.join(agentsDir, 'impeccable-asset-producer.md'), 'utf-8')).toBe(
|
||||
'run `node "${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/embed-prompt.mjs" <asset>`',
|
||||
);
|
||||
});
|
||||
|
||||
test('is a no-op on a missing directory', () => {
|
||||
expect(() => rewritePluginMarkdownTree(path.join(root, 'does-not-exist'))).not.toThrow();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user