Skip to content

Commit 12935e4

Browse files
author
agent-core-bot
committed
chore: sync core lib and CLAUDE.md from agent-core
1 parent 8d569c4 commit 12935e4

2 files changed

Lines changed: 410 additions & 1 deletion

File tree

lib/adapter-transforms.js

Lines changed: 366 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,10 +297,375 @@ function transformForCodex(content, options) {
297297
return content;
298298
}
299299

300+
/**
301+
* Transform content for Cursor (.mdc rule files).
302+
*
303+
* MDC format uses YAML frontmatter with `description`, `globs`, and
304+
* `alwaysApply` fields followed by a markdown body.
305+
*
306+
* @param {string} content - Source markdown content (may have frontmatter)
307+
* @param {Object} options
308+
* @param {string} options.description - Rule description
309+
* @param {string} options.pluginInstallPath - Absolute path to plugin install dir
310+
* @param {string} [options.globs] - Optional glob pattern for file matching
311+
* @param {boolean} [options.alwaysApply] - Whether rule always applies (default true)
312+
* @returns {string} Transformed MDC content
313+
*/
314+
function transformRuleForCursor(content, options) {
315+
const { description = '', pluginInstallPath, globs = '', alwaysApply = true } = options;
316+
317+
// Strip control characters and escape description for YAML
318+
const cleanDescription = description.replace(/[\x00-\x1f\x7f]/g, ' ');
319+
const escapedDescription = cleanDescription.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
320+
const yamlDescription = `"${escapedDescription}"`;
321+
322+
// Build MDC frontmatter
323+
let frontmatter = `---\ndescription: ${yamlDescription}\n`;
324+
if (globs) {
325+
frontmatter += `globs: ${JSON.stringify(globs)}\n`;
326+
}
327+
frontmatter += `alwaysApply: ${alwaysApply}\n---\n`;
328+
329+
// Strip existing frontmatter if present
330+
if (content.startsWith('---')) {
331+
content = content.replace(/^---\n[\s\S]*?\n---\n?/, '');
332+
}
333+
334+
content = frontmatter + content;
335+
336+
// Replace PLUGIN_ROOT paths with actual install path
337+
// Use function replacement to avoid $ pattern interpretation in replacement string
338+
content = content.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, () => pluginInstallPath);
339+
content = content.replace(/\$CLAUDE_PLUGIN_ROOT/g, () => pluginInstallPath);
340+
content = content.replace(/\$\{PLUGIN_ROOT\}/g, () => pluginInstallPath);
341+
content = content.replace(/\$PLUGIN_ROOT/g, () => pluginInstallPath);
342+
343+
// Strip Claude-specific syntax: Task tool calls (handles one level of nested braces)
344+
content = content.replace(/await\s+Task\s*\(\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}\s*\);?/g, (match) => {
345+
const agentMatch = match.match(/subagent_type:\s*["'](?:[^"':]+:)?([^"']+)["']/);
346+
if (agentMatch) {
347+
return `Invoke the ${agentMatch[1]} agent`;
348+
}
349+
return '';
350+
});
351+
352+
// Strip require() statements
353+
content = content.replace(/(?:const|let|var)\s+\{?[^}=\n]+\}?\s*=\s*require\s*\([^)]+\);?/g, '');
354+
content = content.replace(/require\s*\(['"][^'"]+['"]\)/g, '');
355+
356+
// Strip plugin namespacing (e.g. next-task:agent-name -> agent-name)
357+
content = content.replace(/(?:next-task|deslop|ship|sync-docs|audit-project|enhance|perf|repo-map|drift-detect|consult|debate|learn|web-ctl):([a-z][a-z0-9-]*)/g, '$1');
358+
359+
return content;
360+
}
361+
362+
/**
363+
* Transform skill content for Cursor.
364+
*
365+
* Minimal transform - Cursor reads SKILL.md frontmatter natively so we
366+
* preserve it. Only replaces PLUGIN_ROOT paths and strips namespace prefixes.
367+
*
368+
* @param {string} content - Source SKILL.md content
369+
* @param {Object} options
370+
* @param {string} options.pluginInstallPath - Absolute path to plugin install dir
371+
* @returns {string} Transformed skill content
372+
*/
373+
function transformSkillForCursor(content, options) {
374+
const { pluginInstallPath } = options;
375+
376+
// Replace PLUGIN_ROOT paths with actual install path
377+
content = content.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, () => pluginInstallPath);
378+
content = content.replace(/\$CLAUDE_PLUGIN_ROOT/g, () => pluginInstallPath);
379+
content = content.replace(/\$\{PLUGIN_ROOT\}/g, () => pluginInstallPath);
380+
content = content.replace(/\$PLUGIN_ROOT/g, () => pluginInstallPath);
381+
382+
// Strip plugin namespacing (e.g. next-task:agent-name -> agent-name)
383+
content = content.replace(/(?:next-task|deslop|ship|sync-docs|audit-project|enhance|perf|repo-map|drift-detect|consult|debate|learn|web-ctl):([a-z][a-z0-9-]*)/g, '$1');
384+
385+
return content;
386+
}
387+
388+
/**
389+
* Transform command content for Cursor.
390+
*
391+
* Light transform - strips frontmatter, replaces PLUGIN_ROOT paths,
392+
* removes require() statements and Task() calls, strips namespace prefixes.
393+
*
394+
* @param {string} content - Source command markdown content
395+
* @param {Object} options
396+
* @param {string} options.pluginInstallPath - Absolute path to plugin install dir
397+
* @returns {string} Transformed command content
398+
*/
399+
function transformCommandForCursor(content, options) {
400+
const { pluginInstallPath } = options;
401+
402+
// Strip existing frontmatter if present
403+
if (content.startsWith('---')) {
404+
content = content.replace(/^---\n[\s\S]*?\n---\n?/, '');
405+
}
406+
407+
// Replace PLUGIN_ROOT paths with actual install path
408+
content = content.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, () => pluginInstallPath);
409+
content = content.replace(/\$CLAUDE_PLUGIN_ROOT/g, () => pluginInstallPath);
410+
content = content.replace(/\$\{PLUGIN_ROOT\}/g, () => pluginInstallPath);
411+
content = content.replace(/\$PLUGIN_ROOT/g, () => pluginInstallPath);
412+
413+
// Strip require() statements
414+
content = content.replace(/(?:const|let|var)\s+\{?[^}=\n]+\}?\s*=\s*require\s*\([^)]+\);?/g, '');
415+
content = content.replace(/require\s*\(['"][^'"]+['"]\)/g, '');
416+
417+
// Strip Claude-specific syntax: Task tool calls (handles one level of nested braces)
418+
content = content.replace(/await\s+Task\s*\(\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}\s*\);?/g, (match) => {
419+
const agentMatch = match.match(/subagent_type:\s*["'](?:[^"':]+:)?([^"']+)["']/);
420+
if (agentMatch) {
421+
return `Invoke the ${agentMatch[1]} agent`;
422+
}
423+
return '';
424+
});
425+
426+
// Strip plugin namespacing (e.g. next-task:agent-name -> agent-name)
427+
content = content.replace(/(?:next-task|deslop|ship|sync-docs|audit-project|enhance|perf|repo-map|drift-detect|consult|debate|learn|web-ctl):([a-z][a-z0-9-]*)/g, '$1');
428+
429+
return content;
430+
}
431+
432+
/**
433+
* Transform skill content for Kiro.
434+
*
435+
* Minimal transform - Kiro reads standard SKILL.md format natively so we
436+
* preserve it. Only replaces PLUGIN_ROOT paths and strips namespace prefixes.
437+
*
438+
* @param {string} content - Source SKILL.md content
439+
* @param {Object} options
440+
* @param {string} options.pluginInstallPath - Absolute path to plugin install dir
441+
* @returns {string} Transformed skill content
442+
*/
443+
function transformSkillForKiro(content, options) {
444+
const { pluginInstallPath } = options;
445+
446+
content = content.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, () => pluginInstallPath);
447+
content = content.replace(/\$CLAUDE_PLUGIN_ROOT/g, () => pluginInstallPath);
448+
content = content.replace(/\$\{PLUGIN_ROOT\}/g, () => pluginInstallPath);
449+
content = content.replace(/\$PLUGIN_ROOT/g, () => pluginInstallPath);
450+
451+
content = content.replace(/(?:next-task|deslop|ship|sync-docs|audit-project|enhance|perf|repo-map|drift-detect|consult|debate|learn|web-ctl):([a-z][a-z0-9-]*)/g, '$1');
452+
453+
return content;
454+
}
455+
456+
/**
457+
* Transform command content for Kiro prompt files.
458+
*
459+
* Strips existing frontmatter, prepends inclusion: manual frontmatter,
460+
* replaces PLUGIN_ROOT paths, removes require()/Task() calls, strips namespaces.
461+
*/
462+
function transformCommandForKiro(content, options) {
463+
const { pluginInstallPath, name = '', description = '' } = options;
464+
465+
if (content.startsWith('---')) {
466+
content = content.replace(/^---\n[\s\S]*?\n---\n?/, '');
467+
}
468+
469+
const cleanDescription = description.replace(/[\x00-\x1f\x7f]/g, ' ');
470+
const escapedDescription = cleanDescription.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
471+
let frontmatter = '---\n';
472+
frontmatter += 'inclusion: manual\n';
473+
if (name) frontmatter += `name: "${name}"\n`;
474+
if (description) frontmatter += `description: "${escapedDescription}"\n`;
475+
frontmatter += '---\n';
476+
477+
content = frontmatter + content;
478+
479+
content = content.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, () => pluginInstallPath);
480+
content = content.replace(/\$CLAUDE_PLUGIN_ROOT/g, () => pluginInstallPath);
481+
content = content.replace(/\$\{PLUGIN_ROOT\}/g, () => pluginInstallPath);
482+
content = content.replace(/\$PLUGIN_ROOT/g, () => pluginInstallPath);
483+
484+
content = content.replace(/(?:const|let|var)\s+\{?[^}=\n]+\}?\s*=\s*require\s*\([^)]+\);?/g, '');
485+
content = content.replace(/require\s*\(['"][^'"]+['"]\)/g, '');
486+
487+
// Transform code blocks containing Promise.all + Task() (parallel reviewer spawns).
488+
// The bare Task() regex below can't reach inside fenced code blocks.
489+
// Fence boundaries must be at line start (^```) to avoid matching backtick
490+
// template literals inside the code as false fence endings.
491+
content = content.replace(/^```(?:javascript|js)?\n([\s\S]*?)^```$/gm, (fullBlock, codeContent) => {
492+
if (!codeContent.includes('Promise.all') || !codeContent.includes('Task(')) return fullBlock;
493+
// Use lazy [\s\S]*? for the template literal body: it stops at the first backtick and
494+
// naturally matches any character including standalone $ (e.g. $100, $BUDGET).
495+
const taskMatches = [...codeContent.matchAll(/Task\s*\(\s*\{[\s\S]*?subagent_type:\s*['"](?:[^"':]+:)?([^'"]+)['"][\s\S]*?prompt:\s*`([\s\S]*?)`/gs)];
496+
if (taskMatches.length < 2) return fullBlock;
497+
498+
const delegations = taskMatches.map(m => {
499+
const agent = m[1];
500+
const promptFirstLine = m[2].split('\n').find(l => l.trim()) || '';
501+
return `Delegate to the \`${agent}\` subagent:\n> ${promptFirstLine.trim()}`;
502+
});
503+
504+
let result = delegations.join('\n\n');
505+
506+
const hasReviewKeyword = delegations.some(d =>
507+
/review|quality|security|performance|test|coverage/i.test(d)
508+
);
509+
if (delegations.length >= 4 && hasReviewKeyword) {
510+
result = `**Review phase (Kiro - max 4 agents, fallback to 2 sequential):**\n\nTry delegating to these subagents (experimental parallel spawning):\n\n${result}\n\nIf parallel spawning is unavailable, run 2 combined reviewers sequentially:\n1. Delegate to the \`reviewer-quality-security\` subagent (code quality + security)\n2. Then delegate to the \`reviewer-perf-test\` subagent (performance + test coverage)\n\nAggregate all findings from whichever execution path succeeded.`;
511+
}
512+
513+
return result;
514+
});
515+
516+
// Transform bare Task() calls outside code blocks
517+
content = content.replace(/await\s+Task\s*\(\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}\s*\);?/g, (match) => {
518+
const agentMatch = match.match(/subagent_type:\s*["'](?:[^"':]+:)?([^"']+)["']/);
519+
const promptMatch = match.match(/prompt:\s*[`"']([\s\S]*?)[`"']/);
520+
if (agentMatch) {
521+
const agentName = agentMatch[1];
522+
const prompt = promptMatch ? promptMatch[1].replace(/\\n/g, '\n').trim() : '';
523+
if (prompt) {
524+
return `Delegate to the \`${agentName}\` subagent:\n> ${prompt.split('\n')[0]}`;
525+
}
526+
return `Delegate to the \`${agentName}\` subagent.`;
527+
}
528+
return '';
529+
});
530+
531+
// Transform AskUserQuestion to markdown prompt for Kiro chat
532+
content = content.replace(/(?:await\s+)?AskUserQuestion\s*\(\s*\{[\s\S]*?\}\s*\);?/g, (match) => {
533+
const questionMatch = match.match(/question:\s*["'`]([\s\S]*?)["'`]/);
534+
const question = questionMatch ? questionMatch[1] : 'Please choose:';
535+
const optionMatches = [...match.matchAll(/label:\s*["'`]([^"'`]+)["'`][\s\S]*?description:\s*["'`]([^"'`]+)["'`]/g)];
536+
if (optionMatches.length > 0) {
537+
const options = optionMatches.map((m, i) => `${i + 1}. **${m[1]}** - ${m[2]}`).join('\n');
538+
return `**${question}**\n\n${options}\n\nReply with the number or name of your choice.`;
539+
}
540+
return `**${question}**\n\nReply in chat with your choice.`;
541+
});
542+
543+
content = content.replace(/(?:next-task|deslop|ship|sync-docs|audit-project|enhance|perf|repo-map|drift-detect|consult|debate|learn|web-ctl):([a-z][a-z0-9-]*)/g, '$1');
544+
545+
// Batch parallel reviewer delegations for Kiro's agent limit.
546+
// Detects 4+ consecutive "Delegate to" lines with review-related names
547+
// and rewrites as try-4-then-fallback-to-2 pattern.
548+
const reviewerBatchPattern = /((?:Delegate to the `[^`]*` subagent[^\n]*\n){4,})/g;
549+
content = content.replace(reviewerBatchPattern, (block) => {
550+
const delegations = block.match(/Delegate to the `([^`]+)` subagent/g) || [];
551+
if (delegations.length < 4) return block;
552+
553+
const hasReviewKeyword = delegations.some(d =>
554+
/review|quality|security|performance|test|coverage/i.test(d)
555+
);
556+
if (!hasReviewKeyword) return block;
557+
558+
return `**Review phase (Kiro - max 4 agents, fallback to 2 sequential):**
559+
560+
Try delegating to these subagents (experimental parallel spawning):
561+
${block}
562+
If parallel spawning is unavailable, run 2 combined reviewers sequentially:
563+
1. Delegate to the \`reviewer-quality-security\` subagent (code quality + security)
564+
2. Then delegate to the \`reviewer-perf-test\` subagent (performance + test coverage)
565+
566+
Aggregate all findings from whichever execution path succeeded.\n`;
567+
});
568+
569+
return content;
570+
}
571+
572+
/**
573+
* Transform agent markdown+frontmatter to Kiro JSON format.
574+
*
575+
* Parses frontmatter for name/description/model/tools, uses body as prompt.
576+
* Returns a JSON string matching Kiro's agent schema.
577+
*/
578+
function transformAgentForKiro(content, options) {
579+
const { pluginInstallPath } = options || {};
580+
581+
const frontmatter = discovery.parseFrontmatter(content);
582+
let body = content;
583+
if (content.startsWith('---')) {
584+
const endIdx = content.indexOf('\n---', 3);
585+
if (endIdx !== -1) {
586+
body = content.substring(endIdx + 4).replace(/^\n/, '');
587+
}
588+
}
589+
590+
if (pluginInstallPath) {
591+
body = body.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, () => pluginInstallPath);
592+
body = body.replace(/\$CLAUDE_PLUGIN_ROOT/g, () => pluginInstallPath);
593+
body = body.replace(/\$\{PLUGIN_ROOT\}/g, () => pluginInstallPath);
594+
body = body.replace(/\$PLUGIN_ROOT/g, () => pluginInstallPath);
595+
}
596+
597+
body = body.replace(/(?:next-task|deslop|ship|sync-docs|audit-project|enhance|perf|repo-map|drift-detect|consult|debate|learn|web-ctl):([a-z][a-z0-9-]*)/g, '$1');
598+
599+
const agent = {
600+
name: frontmatter.name || '',
601+
description: frontmatter.description || '',
602+
prompt: body.trim()
603+
};
604+
605+
if (frontmatter.tools) {
606+
// parseFrontmatter returns arrays for YAML list syntax, string for inline
607+
const toolItems = Array.isArray(frontmatter.tools)
608+
? frontmatter.tools.map(t => t.toLowerCase())
609+
: [frontmatter.tools.toLowerCase()];
610+
const toolStr = toolItems.join(' ');
611+
const tools = [];
612+
if (toolStr.includes('read')) tools.push('read');
613+
if (toolStr.includes('edit') || toolStr.includes('write')) tools.push('write');
614+
if (toolStr.includes('bash') || toolStr.includes('shell')) tools.push('shell');
615+
if (toolStr.includes('glob')) tools.push('read');
616+
if (toolStr.includes('grep')) tools.push('read');
617+
if (toolStr.includes('task') || toolStr.includes('agent')) tools.push('shell');
618+
if (toolStr.includes('web') || toolStr.includes('fetch')) tools.push('shell');
619+
if (toolStr.includes('notebook')) tools.push('write');
620+
if (toolStr.includes('lsp')) tools.push('read');
621+
const deduped = [...new Set(tools)];
622+
agent.tools = deduped.length > 0 ? deduped : ['read'];
623+
} else {
624+
agent.tools = ['read'];
625+
}
626+
627+
agent.resources = ['file://.kiro/prompts/**/*.md'];
628+
629+
return JSON.stringify(agent, null, 2);
630+
}
631+
632+
/**
633+
* Generate a combined reviewer agent JSON for Kiro's 4-agent limit.
634+
* Merges multiple review responsibilities into a single agent.
635+
*
636+
* @param {Array<{name: string, focus: string}>} roles - Review passes to combine
637+
* @param {string} name - Agent name
638+
* @param {string} description - Agent description
639+
* @returns {string} JSON string for .kiro/agents/*.json
640+
*/
641+
function generateCombinedReviewerAgent(roles, name, description) {
642+
const sections = roles.map(r =>
643+
`## ${r.name} Review\n\nFocus: ${r.focus}`
644+
).join('\n\n---\n\n');
645+
646+
const agent = {
647+
name,
648+
description,
649+
prompt: `You are a combined code reviewer covering multiple review passes in a single session.\n\n${sections}\n\nFor each file you review, check ALL of the above review dimensions. Return findings as a JSON array with objects containing: pass (which review), file, line, severity (critical/high/medium/low), description, suggestion.`,
650+
tools: ['read'],
651+
resources: ['file://.kiro/prompts/**/*.md'],
652+
};
653+
654+
return JSON.stringify(agent, null, 2);
655+
}
656+
300657
module.exports = {
301658
transformBodyForOpenCode,
302659
transformCommandFrontmatterForOpenCode,
303660
transformAgentFrontmatterForOpenCode,
304661
transformSkillBodyForOpenCode,
305-
transformForCodex
662+
transformForCodex,
663+
transformRuleForCursor,
664+
transformSkillForCursor,
665+
transformCommandForCursor,
666+
transformForCursor: transformRuleForCursor,
667+
transformSkillForKiro,
668+
transformCommandForKiro,
669+
transformAgentForKiro,
670+
generateCombinedReviewerAgent
306671
};

0 commit comments

Comments
 (0)