mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-13 06:36:26 +03:00
build: native subagent pipeline + Codex-only asset producer
Adds an agent cross-compile pipeline alongside the existing skill pipeline. Sources live at skill/agents/*.md; providers that declare agentFormat (codex-toml, claude-md) emit native subagent files. An optional providers: <list> field on an agent gates which harnesses get a copy; default (no field) ships everywhere. The impeccable-asset-producer agent is opt-in to Codex only. It's useful for Codex's native image generation path and is untested elsewhere; Claude has no native image gen anyway. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
b03d7515c8
commit
fd3eed9f3b
+31
-1
@@ -390,7 +390,7 @@ This folder contains skills for all supported tools:
|
||||
.cursor/ -> Cursor
|
||||
.claude/ -> Claude Code
|
||||
.gemini/ -> Gemini CLI
|
||||
.codex/ -> Legacy bundle folder in this ZIP (Codex CLI uses .agents/)
|
||||
.codex/ -> Codex custom agents (Codex skills use .agents/)
|
||||
.agents/ -> Codex CLI
|
||||
.github/ -> GitHub Copilot
|
||||
.kiro/ -> Kiro
|
||||
@@ -662,6 +662,18 @@ async function build() {
|
||||
}
|
||||
}
|
||||
|
||||
for (const { provider, configDir, agentFormat } of Object.values(PROVIDERS)) {
|
||||
if (!agentFormat) continue;
|
||||
|
||||
const agentsSrc = path.join(DIST_DIR, provider, configDir, 'agents');
|
||||
const agentsDest = path.join(ROOT_DIR, configDir, 'agents');
|
||||
|
||||
if (fs.existsSync(agentsDest)) fs.rmSync(agentsDest, { recursive: true, force: true });
|
||||
if (fs.existsSync(agentsSrc)) {
|
||||
copyDirSync(agentsSrc, agentsDest);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove deprecated skill stubs from local harness dirs. They exist
|
||||
// in dist/ so the cleanup script can redirect users, but they should
|
||||
// not clutter the repo's own skill directories.
|
||||
@@ -691,15 +703,29 @@ async function build() {
|
||||
const pluginRoot = path.join(ROOT_DIR, 'plugin');
|
||||
const pluginManifestDir = path.join(pluginRoot, '.claude-plugin');
|
||||
const pluginSkillsDir = path.join(pluginRoot, 'skills');
|
||||
const pluginAgentsDir = path.join(pluginRoot, 'agents');
|
||||
if (fs.existsSync(pluginManifestDir)) fs.rmSync(pluginManifestDir, { recursive: true });
|
||||
if (fs.existsSync(pluginSkillsDir)) fs.rmSync(pluginSkillsDir, { recursive: true });
|
||||
if (fs.existsSync(pluginAgentsDir)) fs.rmSync(pluginAgentsDir, { 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');
|
||||
const pluginAgentEntries = fs.existsSync(claudeAgentsSrc)
|
||||
? fs.readdirSync(claudeAgentsSrc)
|
||||
.filter(file => file.endsWith('.md'))
|
||||
.sort()
|
||||
.map(file => `./agents/${file}`)
|
||||
: [];
|
||||
// Trailing slash on the skills path matches the documented schema in
|
||||
// code.claude.com/docs/en/plugins-reference. Issue #86 has 3 reporters
|
||||
// converging on "add trailing slash to fix slash commands not registering";
|
||||
// the docs schema example consistently uses `"./custom/skills/"` form.
|
||||
const pluginManifest = { ...rootManifest, skills: './skills/' };
|
||||
if (pluginAgentEntries.length) {
|
||||
pluginManifest.agents = pluginAgentEntries;
|
||||
} else {
|
||||
delete pluginManifest.agents;
|
||||
}
|
||||
fs.mkdirSync(pluginManifestDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(pluginManifestDir, 'plugin.json'),
|
||||
@@ -712,6 +738,10 @@ async function build() {
|
||||
copyDirSync(claudeSkillsSrc, path.join(pluginSkillsDir, 'impeccable'));
|
||||
}
|
||||
|
||||
if (fs.existsSync(claudeAgentsSrc)) {
|
||||
copyDirSync(claudeAgentsSrc, pluginAgentsDir);
|
||||
}
|
||||
|
||||
console.log('📦 Built Claude Code plugin subtree at ./plugin/');
|
||||
|
||||
// Generate authoritative counts and validate references
|
||||
|
||||
@@ -65,6 +65,72 @@ function buildOpenAIMetadata(skill) {
|
||||
};
|
||||
}
|
||||
|
||||
function formatTomlString(value) {
|
||||
return JSON.stringify(String(value));
|
||||
}
|
||||
|
||||
function formatTomlMultiline(value) {
|
||||
const normalized = String(value).trim().replace(/\r\n/g, '\n');
|
||||
if (!normalized.includes("'''")) {
|
||||
return `'''\n${normalized}\n'''`;
|
||||
}
|
||||
return `"""\n${normalized.replace(/\\/g, '\\\\').replace(/"""/g, '\\"""')}\n"""`;
|
||||
}
|
||||
|
||||
function formatTomlArray(values) {
|
||||
return `[${values.map(formatTomlString).join(', ')}]`;
|
||||
}
|
||||
|
||||
function buildCodexAgent(agent, body) {
|
||||
const lines = [
|
||||
`name = ${formatTomlString(agent.codexName || agent.name.replace(/-/g, '_'))}`,
|
||||
`description = ${formatTomlString(agent.description)}`,
|
||||
];
|
||||
|
||||
if (agent.effort) {
|
||||
lines.push(`model_reasoning_effort = ${formatTomlString(agent.effort)}`);
|
||||
}
|
||||
|
||||
if (agent.nicknameCandidates?.length) {
|
||||
lines.push(`nickname_candidates = ${formatTomlArray(agent.nicknameCandidates)}`);
|
||||
}
|
||||
|
||||
lines.push(`developer_instructions = ${formatTomlMultiline(body)}`);
|
||||
return `${lines.join('\n')}\n`;
|
||||
}
|
||||
|
||||
function buildClaudeAgent(agent, body) {
|
||||
const frontmatter = {
|
||||
name: agent.claudeName || agent.name,
|
||||
description: agent.description,
|
||||
};
|
||||
|
||||
if (agent.tools) frontmatter.tools = agent.tools;
|
||||
if (agent.model) frontmatter.model = agent.model;
|
||||
if (agent.effort) frontmatter.effort = agent.effort;
|
||||
if (agent.maxTurns) frontmatter.maxTurns = agent.maxTurns;
|
||||
|
||||
return `${generateYamlFrontmatter(frontmatter)}\n${body.trim()}\n`;
|
||||
}
|
||||
|
||||
function buildAgentFile(config, agent, body) {
|
||||
if (config.agentFormat === 'codex-toml') {
|
||||
return {
|
||||
filename: `${agent.codexName || agent.name.replace(/-/g, '_')}.toml`,
|
||||
content: buildCodexAgent(agent, body),
|
||||
};
|
||||
}
|
||||
|
||||
if (config.agentFormat === 'claude-md') {
|
||||
return {
|
||||
filename: `${agent.claudeName || agent.name}.md`,
|
||||
content: buildClaudeAgent(agent, body),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a transformer function for a given provider config.
|
||||
*
|
||||
@@ -94,6 +160,7 @@ export function createTransformer(config) {
|
||||
|
||||
let refCount = 0;
|
||||
let scriptCount = 0;
|
||||
let agentCount = 0;
|
||||
|
||||
for (const skill of skills) {
|
||||
const skillName = skill.name;
|
||||
@@ -171,9 +238,27 @@ export function createTransformer(config) {
|
||||
}
|
||||
}
|
||||
|
||||
if (config.agentFormat) {
|
||||
const agentsDir = path.join(providerDir, `${configDir}/agents`);
|
||||
for (const skill of skills) {
|
||||
for (const agent of skill.agents || []) {
|
||||
// Agents can declare `providers: <list>` to limit which harnesses
|
||||
// they emit to. Default (no field) ships everywhere with agentFormat.
|
||||
if (agent.providers && !agent.providers.includes(provider)) continue;
|
||||
const body = replacePlaceholders(agent.body, placeholderKey, [], allSkillNames);
|
||||
const agentFile = buildAgentFile(config, agent, body);
|
||||
if (!agentFile) continue;
|
||||
ensureDir(agentsDir);
|
||||
writeFile(path.join(agentsDir, agentFile.filename), agentFile.content);
|
||||
agentCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const skillWord = skills.length === 1 ? 'skill' : 'skills';
|
||||
const refInfo = refCount > 0 ? ` (${refCount} reference files)` : '';
|
||||
const scriptInfo = scriptCount > 0 ? ` (${scriptCount} script files)` : '';
|
||||
console.log(`✓ ${displayName}: ${skills.length} ${skillWord}${refInfo}${scriptInfo}`);
|
||||
const agentInfo = agentCount > 0 ? ` (${agentCount} agent files)` : '';
|
||||
console.log(`✓ ${displayName}: ${skills.length} ${skillWord}${refInfo}${scriptInfo}${agentInfo}`);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export const PROVIDERS = {
|
||||
configDir: '.claude',
|
||||
displayName: 'Claude Code',
|
||||
frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata', 'allowed-tools'],
|
||||
agentFormat: 'claude-md',
|
||||
},
|
||||
gemini: {
|
||||
provider: 'gemini',
|
||||
@@ -34,6 +35,7 @@ export const PROVIDERS = {
|
||||
frontmatterFields: [],
|
||||
includeVersion: false,
|
||||
writeOpenAIMetadata: true,
|
||||
agentFormat: 'codex-toml',
|
||||
},
|
||||
agents: {
|
||||
provider: 'agents',
|
||||
|
||||
+35
-1
@@ -203,6 +203,39 @@ export function readSourceFiles(rootDir) {
|
||||
}
|
||||
}
|
||||
|
||||
const agents = [];
|
||||
const agentsDir = path.join(skillDir, 'agents');
|
||||
if (fs.existsSync(agentsDir)) {
|
||||
const agentFiles = fs.readdirSync(agentsDir).filter(f => f.endsWith('.md'));
|
||||
for (const agentFile of agentFiles) {
|
||||
const agentPath = path.join(agentsDir, agentFile);
|
||||
const agentContent = fs.readFileSync(agentPath, 'utf-8');
|
||||
const { frontmatter: agentFrontmatter, body: agentBody } = parseFrontmatter(agentContent);
|
||||
const name = agentFrontmatter.name || path.basename(agentFile, '.md');
|
||||
const providersRaw = agentFrontmatter.providers;
|
||||
let providers = null;
|
||||
if (Array.isArray(providersRaw)) {
|
||||
providers = providersRaw.map(p => String(p).trim()).filter(Boolean);
|
||||
} else if (typeof providersRaw === 'string' && providersRaw.trim()) {
|
||||
providers = providersRaw.split(',').map(p => p.trim()).filter(Boolean);
|
||||
}
|
||||
agents.push({
|
||||
name,
|
||||
codexName: agentFrontmatter['codex-name'] || name.replace(/-/g, '_'),
|
||||
claudeName: agentFrontmatter['claude-name'] || name,
|
||||
description: agentFrontmatter.description || '',
|
||||
tools: agentFrontmatter.tools || '',
|
||||
model: agentFrontmatter.model || '',
|
||||
effort: agentFrontmatter.effort || '',
|
||||
maxTurns: agentFrontmatter['max-turns'] ? Number(agentFrontmatter['max-turns']) : '',
|
||||
nicknameCandidates: agentFrontmatter['nickname-candidates'] || [],
|
||||
providers,
|
||||
body: agentBody,
|
||||
filePath: agentPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
skills.push({
|
||||
name: frontmatter.name || 'impeccable',
|
||||
description: frontmatter.description || '',
|
||||
@@ -216,7 +249,8 @@ export function readSourceFiles(rootDir) {
|
||||
body,
|
||||
filePath: skillMdPath,
|
||||
references,
|
||||
scripts
|
||||
scripts,
|
||||
agents
|
||||
});
|
||||
|
||||
return { skills };
|
||||
|
||||
Reference in New Issue
Block a user