Define the agent embed behavior when the plugin-root variable is unexpanded

The current plugins reference substitutes ${CLAUDE_PLUGIN_ROOT} inline
anywhere in plugin skill and agent content, superseding the hook-only
note this module carried from earlier research (review finding cited it).
The module header now states the actual contract, and because Grok Build
reads the same subtree with its own substitution behavior, the agent's
embed instruction gains a defined fallback: when the script path is
unreachable, the prompt survives as an <asset>.prompt.txt sidecar and the
manifest tells the parent, whose thread can resolve the script and embed
properly. A new build guard fails when an agent file carries an
unresolvable path form or an embed instruction whose fallback anchor
drifted.

Drafted with AI assistance, reviewed by a maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Abdul Wahab
2026-08-28 05:18:06 +05:00
co-authored by Cursor
parent d28a9dc31c
commit 3611594092
4 changed files with 123 additions and 14 deletions
+1 -1
View File
@@ -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 "${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.
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. When that script path is unreachable in your environment, write the same prompt to `<asset>.prompt.txt` beside the asset and note it in your manifest so the parent can embed it. 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.
+6
View File
@@ -28,6 +28,7 @@ import {
rewritePluginMarkdownTree,
rewritePluginAgentMarkdown,
verifyPluginSkillRewrite,
verifyPluginAgentRewrite,
} from './lib/plugin-paths.js';
import { stageOpenAIPlugin } from './lib/openai-plugin.js';
import { ANTIPATTERNS } from '../cli/engine/registry/antipatterns.mjs';
@@ -763,6 +764,11 @@ async function build() {
// a spawned agent never loads SKILL.md, so the token is undefined there.
rewritePluginMarkdownTree(pluginAgentsDir, rewritePluginAgentMarkdown);
verifyPluginSkillRewrite(path.join(pluginSkillsDir, 'impeccable', 'SKILL.md'));
if (fs.existsSync(pluginAgentsDir)) {
for (const agentFile of fs.readdirSync(pluginAgentsDir)) {
if (agentFile.endsWith('.md')) verifyPluginAgentRewrite(path.join(pluginAgentsDir, agentFile));
}
}
// Ship the design detector as a plugin-packaged hook. Claude Code and
// Grok Build both auto-discover `hooks/hooks.json` at the plugin root
+54 -8
View File
@@ -11,11 +11,13 @@ import path from 'path';
* gets MODULE_NOT_FOUND, and a dual-install user silently runs the
* project's (possibly older) skill copy.
*
* There is no literal path that works for plugins (CLAUDE_PLUGIN_ROOT is
* hook-only), so the plugin's markdown uses the `<skill-base-dir>` form
* SKILL.md's Setup step 1 already leads with: the runtime shows the
* skill's loaded base directory when it loads the skill, and scripts
* resolve against that.
* No literal path survives installation (the plugin cache location varies
* per machine and per plugin version), so skill and reference markdown
* uses the `<skill-base-dir>` form SKILL.md's Setup step 1 already leads
* with: the runtime shows the skill's loaded base directory when it loads
* the skill, and scripts resolve against that. Agent files cannot use the
* token (a spawned agent never loads SKILL.md) and get the
* ${CLAUDE_PLUGIN_ROOT} variable instead; see PLUGIN_AGENT_SCRIPTS_PATH.
*/
// The resolved {{scripts_path}} in dist/claude-code output, fixed by the
@@ -48,11 +50,23 @@ const SETUP_PLUGIN_TEXT =
// 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.
// per the substitution table in code.claude.com/docs/en/plugins-reference.
// (anthropics/claude-code#65768 observed subagents receiving the literal;
// it was auto-closed stale and the docs table postdates it. Frontmatter
// still has no variable, which is why the node pre-approval is dropped
// rather than rewritten.) Grok Build reads this same subtree with its own
// substitution behavior, so the embed instruction carries a sidecar
// fallback for any harness that hands the agent the unexpanded literal.
export const PLUGIN_AGENT_SCRIPTS_PATH = '${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts';
// Appended as its own sentence after the agent's embed instruction so
// behavior is defined even where the variable reaches the agent
// unexpanded: the prompt survives as a sidecar and the manifest tells the
// parent, whose own thread can resolve the script and embed it properly.
export const AGENT_EMBED_FALLBACK =
' When that script path is unreachable in your environment, write the same prompt to ' +
'`<asset>.prompt.txt` beside the asset and note it in your manifest so the parent can embed it.';
/**
* Rewrite one markdown file's content for the plugin subtree. Pure, so the
* unit suite can pin every rewrite without a build.
@@ -85,9 +99,41 @@ export function rewritePluginAgentMarkdown(content) {
.replace(
/node \$\{CLAUDE_PLUGIN_ROOT\}\/skills\/impeccable\/scripts\/([^\s`"]+)/g,
'node "${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/$1"',
)
// Anchors on the command this rewrite just produced plus the rest of
// its sentence, so the fallback lands as the following sentence rather
// than splicing into the middle of one.
.replace(
/(`node "\$\{CLAUDE_PLUGIN_ROOT\}\/skills\/impeccable\/scripts\/embed-prompt\.mjs"[^`]*`[^.]*\.)/g,
`$1${AGENT_EMBED_FALLBACK}`,
);
}
/**
* Fail the build when an agent file's rewrite no longer holds: a
* project-relative scripts path or the skill-base-dir token survived, or
* an embed instruction lost its unexpanded-variable fallback because the
* source sentence the anchor keys on was reworded. Loud beats a silent
* no-op, same contract as verifyPluginSkillRewrite.
*/
export function verifyPluginAgentRewrite(agentPath) {
const content = fs.readFileSync(agentPath, 'utf-8');
if (content.includes(CLAUDE_PROJECT_SCRIPTS_PATH) || content.includes('<skill-base-dir>')) {
throw new Error(
`Plugin rewrite drift: ${agentPath} references a scripts path a spawned agent cannot ` +
'resolve (the project-relative form or the <skill-base-dir> token). Agent files must ' +
'carry the ${CLAUDE_PLUGIN_ROOT} form; see rewritePluginAgentMarkdown (issue #523).',
);
}
if (content.includes('embed-prompt.mjs') && !content.includes(AGENT_EMBED_FALLBACK)) {
throw new Error(
`Plugin rewrite drift: ${agentPath} carries an embed instruction without the sidecar ` +
"fallback sentence. The source sentence no longer matches the anchor in " +
'scripts/lib/plugin-paths.js (issue #523); update the fallback anchor to the new wording.',
);
}
}
/**
* 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
+62 -5
View File
@@ -20,7 +20,9 @@ import {
rewritePluginAgentMarkdown,
rewritePluginMarkdownTree,
verifyPluginSkillRewrite,
verifyPluginAgentRewrite,
CLAUDE_PROJECT_SCRIPTS_PATH,
AGENT_EMBED_FALLBACK,
} from '../scripts/lib/plugin-paths.js';
describe('rewritePluginMarkdown', () => {
@@ -94,25 +96,80 @@ describe('rewritePluginMarkdown', () => {
});
describe('rewritePluginAgentMarkdown', () => {
// The real source sentence shape: command, purpose clause, next sentence.
const sourceStep =
'after every generation, run `node .claude/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.';
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(
const output = rewritePluginAgentMarkdown(sourceStep);
expect(output).toContain(
'run `node "${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/embed-prompt.mjs" <asset> --prompt "<the prompt used>"`',
);
});
test('appends the sidecar fallback as its own sentence, not mid-sentence', () => {
const output = rewritePluginAgentMarkdown(sourceStep);
// The fallback follows the full embed sentence and precedes the next one.
expect(output).toContain(
`so the prompt lives inside the image itself.${AGENT_EMBED_FALLBACK} The build thread`,
);
});
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);
const output = rewritePluginAgentMarkdown(sourceStep);
expect(output).not.toContain('<skill-base-dir>');
expect(output).not.toContain(CLAUDE_PROJECT_SCRIPTS_PATH);
});
});
describe('verifyPluginAgentRewrite', () => {
let root;
beforeEach(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-agent-verify-'));
});
afterEach(() => {
fs.rmSync(root, { recursive: true, force: true });
});
const writeAgent = (contents) => {
const p = path.join(root, 'agent.md');
fs.writeFileSync(p, contents);
return p;
};
const sourceStep =
'run `node .claude/skills/impeccable/scripts/embed-prompt.mjs <asset> --prompt "<p>"` ' +
'so the prompt lives inside the image itself. Next sentence.';
test('accepts a correctly rewritten agent file', () => {
const p = writeAgent(rewritePluginAgentMarkdown(sourceStep));
expect(() => verifyPluginAgentRewrite(p)).not.toThrow();
});
test('fails when an unresolvable path form survives', () => {
const p = writeAgent('run `node "<skill-base-dir>/scripts/embed-prompt.mjs"` please.');
expect(() => verifyPluginAgentRewrite(p)).toThrow(/cannot\s+resolve/);
});
test('fails when the embed instruction lost its fallback sentence', () => {
// Simulate a source rewording that breaks the fallback anchor: the
// sentence-splice regex no-ops when no period follows the command.
const reworded = sourceStep.replace(
' so the prompt lives inside the image itself. Next sentence.',
' -- no closing period',
);
const p = writeAgent(rewritePluginAgentMarkdown(reworded));
expect(() => verifyPluginAgentRewrite(p)).toThrow(/sidecar/);
});
});
describe('rewritePluginMarkdownTree', () => {
let root;