diff --git a/packages/converters/docs/README.md b/packages/converters/docs/README.md index 5dfa45ef..b055dda6 100644 --- a/packages/converters/docs/README.md +++ b/packages/converters/docs/README.md @@ -26,6 +26,9 @@ Complete overview of all supported formats, their subtypes, and official documen | **Factory Droid** | `skill` | Reusable workflows with YAML frontmatter | [docs.factory.ai](https://docs.factory.ai/cli/configuration/skills) | | | `slash-command` | Custom slash commands with argument hints | [docs.factory.ai](https://docs.factory.ai/cli/configuration/custom-slash-commands) | | | `agents-md` | Agent configurations in markdown | [docs.factory.ai](https://docs.factory.ai/cli/configuration/agents-md) | +| **Codex** | `skill` | Agent Skills SKILL.md format | [developers.openai.com](https://developers.openai.com/codex/skills) | +| | `agent` | Subagent TOML configs with MCP and skills | [developers.openai.com](https://developers.openai.com/codex/multi-agent/) | +| | `rule` | AGENTS.md project instructions | [developers.openai.com](https://developers.openai.com/codex/skills) | | **OpenCode** | `agent` | AI agents with mode, tools, and permissions | [opencode.ai](https://opencode.ai/docs/agents/) | | | `slash-command` | User-triggered prompts with templates and placeholders | [opencode.ai](https://opencode.ai/docs/commands/) | | **Gemini CLI** | `slash-command` | Custom slash commands in TOML format | [geminicli.com](https://geminicli.com/docs/commands/) | @@ -67,6 +70,7 @@ This directory contains detailed specifications for each AI IDE/tool format that | **Kiro Agents** | [kiro-agents.md](./kiro-agents.md) | Custom AI agent configurations (JSON) | [kiro.dev/docs](https://kiro.dev/docs/cli/custom-agents/) | | **Ruler** | [ruler.md](./ruler.md) | Plain markdown rules, centralized management | [okigu.com/ruler](https://okigu.com/ruler) | | **Factory Droid** | [factory-droid.md](./factory-droid.md) | Skills, slash commands, and hooks | [docs.factory.ai](https://docs.factory.ai/) | +| **Codex** | [codex.md](./codex.md) | Skills, subagents, and AGENTS.md instructions | [developers.openai.com](https://developers.openai.com/codex/multi-agent/) | | **OpenCode** | [opencode.md](./opencode.md) | Agents and slash commands with YAML frontmatter | [opencode.ai/docs](https://opencode.ai/docs/) | | **Gemini CLI** | [gemini-plugin.md](./gemini-plugin.md) | Extensions with MCP servers and custom commands | [geminicli.com/docs](https://geminicli.com/docs/extensions/) | | **agents.md** | [agents-md.md](./agents-md.md) | OpenAI format, plain markdown | [github.com/openai/agents.md](https://github.com/openai/agents.md) | @@ -125,6 +129,10 @@ Each format has a corresponding JSON Schema in `../schemas/` that defines the st - `droid-slash-command.schema.json` - Custom slash commands - `droid-hook.schema.json` - Event-driven automations (JSON) +**Codex Subtypes:** +- `codex-agent-role.schema.json` - Subagent TOML configurations +- `agent-skills.schema.json` - Agent Skills SKILL.md (shared standard) + **OpenCode Subtypes:** - `opencode-slash-command.schema.json` - Template-based commands @@ -198,6 +206,7 @@ These specifications serve as the source of truth for: | Copilot | Markdown headers | none | `applyTo`, `excludeAgent` | | Kiro | YAML (optional) | none | `inclusion`, `fileMatchPattern`, `domain` | | Factory Droid | YAML (required) | `name`, `description` | `argument-hint`, `allowed-tools` | +| Codex Agents | TOML (required) | `name`, `description`, `developer_instructions` | `model`, `model_reasoning_effort`, `sandbox_mode`, `nickname_candidates`, `mcp_servers`, `skills.config` | | OpenCode Agents | YAML (required) | `description`, `mode` | `model`, `temperature`, `prompt`, `tools`, `permission`, `disable` | | OpenCode Commands | YAML (required) | `template` | `description`, `agent`, `model`, `subtask` | | Gemini Extension | JSON (required) | `name`, `version` | `description`, `author`, `mcpServers`, `contextFileName`, `excludeTools`, `experimentalSettings` | @@ -221,6 +230,7 @@ These specifications serve as the source of truth for: | Kiro | `.kiro/steering/*.md` | Multiple files | | Kiro Hooks | `.kiro/hooks/*.json` | Multiple JSON files | | Factory Droid | `.factory/skills/*/SKILL.md`, `.factory/commands/*.md` | Skills in subdirs, commands as files | +| Codex | `.codex/agents/*.toml`, `.agents/skills/*/SKILL.md` | Agents as TOML, skills in subdirs | | OpenCode | `.opencode/agent/*.md`, `.opencode/command/*.md` | Agents and commands as separate files | | Gemini CLI | `.gemini/extensions/*/gemini-extension.json` | Extensions in subdirectories with JSON config | | agents.md | `agents.md` | Single file | diff --git a/packages/converters/docs/codex.md b/packages/converters/docs/codex.md new file mode 100644 index 00000000..2fa50bee --- /dev/null +++ b/packages/converters/docs/codex.md @@ -0,0 +1,181 @@ +# Codex Format Specification + +**File Locations:** +- Skills: `.agents/skills/{skill-name}/SKILL.md` +- Agents/Subagents (project): `.codex/agents/*.toml` +- Agents/Subagents (global): `~/.codex/agents/*.toml` +- Rules/Instructions: `AGENTS.md` +- Slash Commands: `.opencommands/*.md` + +**Format:** TOML (agents), Markdown with YAML frontmatter (skills), plain Markdown (AGENTS.md) +**Official Docs:** https://developers.openai.com/codex/multi-agent/ + +## Overview + +OpenAI Codex CLI supports multiple format types for configuring AI agent behavior: + +1. **Skills** (Agent Skills format): Markdown files with YAML frontmatter following the agentskills.io specification +2. **Agents/Subagents**: TOML configuration files that define specialized agent roles for multi-agent workflows +3. **Rules**: Project-level instructions via AGENTS.md +4. **Slash Commands**: Custom commands via `.opencommands/` + +## Subagent TOML Format + +### Required Fields + +- **`name`** (string): Display name of the agent/subagent +- **`description`** (string): Short description of what this agent does and when to use it +- **`developer_instructions`** (string): System prompt / developer instructions for this agent role + +### Optional Fields + +- **`nickname_candidates`** (string[]): Alternative names the agent can be referred to as +- **`model`** (string): Model identifier to use (e.g., "o3", "o3-mini") +- **`model_reasoning_effort`** (enum): Reasoning effort level — `low`, `medium`, or `high` +- **`sandbox_mode`** (enum): Filesystem/network sandbox policy — `read-only` (default), `workspace-write`, or `danger-full-access` +- **`mcp_servers`** (object): MCP server configurations available to this agent +- **`skills.config`** (object): Skills configuration key-value pairs + +### Global Settings (in codex config, not agent files) + +- `agents.max_threads` (default: 6): Maximum parallel agent threads +- `agents.max_depth` (default: 1): Maximum spawning depth +- `agents.job_max_runtime_seconds`: Maximum runtime per agent job + +### Built-in Agents + +Codex includes three built-in agents: `default`, `worker`, and `explorer`. + +## Skill Format (Agent Skills) + +### Required Fields + +- **`name`** (string): 1-64 chars, lowercase alphanumeric and hyphens, must match parent directory name +- **`description`** (string): 1-1024 chars, explains what skill does and when to use it + +### Optional Fields + +- **`license`** (string): Licensing terms +- **`compatibility`** (string): Environment requirements (max 500 chars) +- **`allowed-tools`** (string): Space-delimited list of pre-approved tools +- **`metadata`** (object): Arbitrary key-value pairs + +## Content Format + +### Subagent TOML + +```toml +name = "Security Reviewer" +description = "Find security vulnerabilities and unsafe code patterns" +developer_instructions = "Focus on OWASP Top 10 vulnerabilities, injection attacks, and authentication issues." +nickname_candidates = ["sec-review", "security"] +model = "o3" +model_reasoning_effort = "high" +sandbox_mode = "read-only" + +[mcp_servers.github] +command = "npx" +args = ["-y", "@modelcontextprotocol/server-github"] + +[skills.config] +focus_areas = "security,authentication" +``` + +### Skill SKILL.md + +```markdown +--- +name: typescript-expert +description: Expert TypeScript development assistance with type safety and best practices. +license: MIT +allowed-tools: Bash(tsc:*) Read Write +--- + +You are an expert TypeScript developer. + +## Guidelines + +- Always use strict type checking +- Prefer `unknown` over `any` +``` + +## Best Practices + +1. Keep `developer_instructions` focused and specific to the agent's role +2. Use `sandbox_mode: "read-only"` for agents that only need to analyze code +3. Provide meaningful `nickname_candidates` for easier agent invocation +4. Configure `model_reasoning_effort` based on task complexity (use "low" for simple tasks) +5. Limit MCP server configurations to only what the agent needs + +## Conversion Notes + +### From Codex to Canonical + +- `name` and `description` from TOML map to package metadata and `codexAgent` metadata +- `developer_instructions` maps to an instructions section +- `model`, `model_reasoning_effort`, `sandbox_mode` are stored in `codexAgent` metadata +- `nickname_candidates`, `mcp_servers`, `skills.config` are preserved in `codexAgent` metadata for roundtrip + +### From Canonical to Codex + +- Package name and description populate the `name` and `description` TOML fields +- Instructions sections map to `developer_instructions` +- Tools sections are skipped with a warning (not supported by agent role TOML) +- Persona sections are skipped with a warning +- `codexAgent` metadata fields are restored for roundtrip fidelity + +## Limitations + +- Agent role TOML does not support tools sections (use `allowed-tools` in skill format instead) +- Persona sections cannot be represented in TOML format +- File reference sections are not supported +- Hook sections are not supported +- `mcp_servers` and `skills.config` are opaque to PRPM — values are preserved but not interpreted + +## Examples + +### Minimal Subagent + +```toml +name = "Code Reviewer" +description = "Reviews code for bugs and best practices" +developer_instructions = "Review code changes for correctness, performance, and security issues." +``` + +### Full-Featured Subagent + +```toml +name = "Security Auditor" +description = "Deep security analysis of code changes" +nickname_candidates = ["sec-audit", "security-check"] +model = "o3" +model_reasoning_effort = "high" +sandbox_mode = "read-only" +developer_instructions = """ +You are a security auditor. Analyze code for: +- OWASP Top 10 vulnerabilities +- Injection attacks (SQL, command, XSS) +- Authentication and authorization issues +- Sensitive data exposure +- Security misconfigurations +""" + +[mcp_servers.semgrep] +command = "npx" +args = ["-y", "semgrep-mcp-server"] + +[skills.config] +severity_threshold = "medium" +``` + +## Related Documentation + +- [Codex Multi-Agent Docs](https://developers.openai.com/codex/multi-agent/) +- [Codex Skills Docs](https://developers.openai.com/codex/skills) +- [Agent Skills Specification](https://agentskills.io/specification) +- [PRPM Format Guide](../../docs/formats.mdx) + +## Changelog + +- **2026-03-17**: Added subagent support (name, description, nickname_candidates, mcp_servers, skills.config) +- **2025-01-15**: Initial Codex format support (skills, agent roles) diff --git a/packages/converters/schemas/codex-agent-role.schema.json b/packages/converters/schemas/codex-agent-role.schema.json index 88611c5f..30ddbda5 100644 --- a/packages/converters/schemas/codex-agent-role.schema.json +++ b/packages/converters/schemas/codex-agent-role.schema.json @@ -1,10 +1,28 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://registry.prpm.dev/api/v1/schemas/codex/agent-role.json", - "title": "Codex Agent Role Format", - "description": "JSON Schema for OpenAI Codex CLI agent role configuration (TOML format, installed to ~/.codex/agents/)", + "title": "Codex Agent Role / Subagent Format", + "description": "JSON Schema for OpenAI Codex CLI agent role and subagent configuration (TOML format, installed to ~/.codex/agents/ or .codex/agents/)", "type": "object", + "required": ["name", "description", "developer_instructions"], "properties": { + "name": { + "type": "string", + "description": "Display name of the agent/subagent" + }, + "description": { + "type": "string", + "description": "Short description of what this agent does and when to use it" + }, + "developer_instructions": { + "type": "string", + "description": "System prompt / developer instructions for this agent role" + }, + "nickname_candidates": { + "type": "array", + "items": { "type": "string" }, + "description": "Alternative names the agent can be referred to as" + }, "model": { "type": "string", "description": "Model identifier to use for this agent role" @@ -14,14 +32,40 @@ "enum": ["low", "medium", "high"], "description": "Reasoning effort level for the model" }, - "developer_instructions": { - "type": "string", - "description": "System prompt / developer instructions for this agent role" - }, "sandbox_mode": { "type": "string", "enum": ["read-only", "workspace-write", "danger-full-access"], "description": "Filesystem/network sandbox policy: read-only (default), workspace-write, or danger-full-access (no sandbox)" + }, + "mcp_servers": { + "type": "object", + "description": "MCP server configurations available to this agent", + "additionalProperties": { + "type": "object", + "properties": { + "command": { "type": "string" }, + "args": { + "type": "array", + "items": { "type": "string" } + }, + "env": { + "type": "object", + "additionalProperties": { "type": "string" } + } + } + } + }, + "skills": { + "type": "object", + "description": "Skills configuration for this agent", + "properties": { + "config": { + "type": "object", + "description": "Skill-specific configuration key-value pairs", + "additionalProperties": true + } + }, + "additionalProperties": true } }, "additionalProperties": false diff --git a/packages/converters/src/__tests__/cross-converters/codex-agent-role.test.ts b/packages/converters/src/__tests__/cross-converters/codex-agent-role.test.ts index 2bb584e1..471e0c5a 100644 --- a/packages/converters/src/__tests__/cross-converters/codex-agent-role.test.ts +++ b/packages/converters/src/__tests__/cross-converters/codex-agent-role.test.ts @@ -209,7 +209,12 @@ describe('Codex Agent Role — schema validation', () => { const validModes = ['read-only', 'workspace-write', 'danger-full-access']; for (const mode of validModes) { - const result = validateFormat('codex', { sandbox_mode: mode }, 'agent'); + const result = validateFormat('codex', { + name: 'test-agent', + description: 'Test agent', + developer_instructions: 'Do something.', + sandbox_mode: mode, + }, 'agent'); expect(result.valid).toBe(true); } }); @@ -222,9 +227,122 @@ describe('Codex Agent Role — schema validation', () => { } }); - it('should accept an agent role with no fields (all optional)', () => { + it('should accept an agent role with only required fields', () => { + const result = validateFormat('codex', { + name: 'test-agent', + description: 'A test agent', + developer_instructions: 'Follow instructions.', + }, 'agent'); + expect(result.valid).toBe(true); + }); + + it('should reject an agent role missing required fields', () => { const result = validateFormat('codex', {}, 'agent'); + expect(result.valid).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// Subagent-specific fields: nickname_candidates, mcp_servers, skills.config +// --------------------------------------------------------------------------- + +describe('Codex Agent Role — subagent fields', () => { + it('should roundtrip nickname_candidates through conversion', () => { + const tomlContent = ` +name = "reviewer" +description = "Code reviewer" +developer_instructions = "Review code." +nickname_candidates = ["rev", "code-review"] +`.trim(); + + const canonical = fromCodexAgentRole(tomlContent, baseMetadata); + const meta = canonical.content.sections.find(s => s.type === 'metadata'); + expect(meta?.type === 'metadata' && meta.data.codexAgent?.nicknameCandidates).toEqual(['rev', 'code-review']); + + const result = toCodex(canonical); + expect(result.content).toContain('nickname_candidates'); + expect(result.content).toContain('rev'); + expect(result.content).toContain('code-review'); + }); + + it('should roundtrip mcp_servers through conversion', () => { + const tomlContent = ` +name = "agent-with-mcp" +description = "Agent with MCP" +developer_instructions = "Use MCP tools." + +[mcp_servers.filesystem] +command = "npx" +args = ["-y", "@modelcontextprotocol/server-filesystem"] + +[mcp_servers.filesystem.env] +ROOT_DIR = "/tmp" +`.trim(); + + const canonical = fromCodexAgentRole(tomlContent, baseMetadata); + const meta = canonical.content.sections.find(s => s.type === 'metadata'); + expect(meta?.type === 'metadata' && meta.data.codexAgent?.mcpServers).toBeDefined(); + + const result = toCodex(canonical); + expect(result.content).toContain('mcp_servers'); + expect(result.content).toContain('filesystem'); + }); + + it('should roundtrip skills.config through conversion', () => { + const tomlContent = ` +name = "skilled-agent" +description = "Agent with skills" +developer_instructions = "Use skills." + +[skills.config] +search = true +analyze = false +`.trim(); + + const canonical = fromCodexAgentRole(tomlContent, baseMetadata); + const meta = canonical.content.sections.find(s => s.type === 'metadata'); + expect(meta?.type === 'metadata' && meta.data.codexAgent?.skillsConfig).toBeDefined(); + + const result = toCodex(canonical); + expect(result.content).toContain('skills'); + expect(result.content).toContain('config'); + }); + + it('should validate a full subagent with all fields against schema', () => { + const parsed = { + name: 'full-agent', + description: 'A fully configured subagent', + developer_instructions: 'Do everything.', + nickname_candidates: ['fa', 'full'], + model: 'o3', + model_reasoning_effort: 'high', + sandbox_mode: 'workspace-write', + mcp_servers: { + fs: { command: 'npx', args: ['-y', 'fs-server'] }, + }, + skills: { config: { linting: true } }, + }; + + const result = validateFormat('codex', parsed, 'agent'); expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it('should preserve name and description through roundtrip', () => { + const tomlContent = ` +name = "My Custom Agent" +description = "Does custom things" +developer_instructions = "Be custom." +`.trim(); + + const canonical = fromCodexAgentRole(tomlContent, baseMetadata); + const meta = canonical.content.sections.find(s => s.type === 'metadata'); + expect(meta?.type === 'metadata' && meta.data.codexAgent?.name).toBe('My Custom Agent'); + expect(meta?.type === 'metadata' && meta.data.codexAgent?.description).toBe('Does custom things'); + + const result = toCodex(canonical); + expect(result.content).toContain('My Custom Agent'); + expect(result.content).toContain('Does custom things'); }); }); diff --git a/packages/converters/src/__tests__/from-codex.test.ts b/packages/converters/src/__tests__/from-codex.test.ts index 84a444e1..1e005a58 100644 --- a/packages/converters/src/__tests__/from-codex.test.ts +++ b/packages/converters/src/__tests__/from-codex.test.ts @@ -469,6 +469,120 @@ developer_instructions = "Find security, correctness, and test risks in code." }); }); + describe('subagent fields', () => { + it('should parse name and description fields', () => { + const content = ` +name = "Security Reviewer" +description = "Find security vulnerabilities in code" +developer_instructions = "Focus on OWASP Top 10." +`.trim(); + + const result = fromCodexAgentRole(content, baseMetadata); + + expect(result.name).toBe('Security Reviewer'); + expect(result.description).toBe('Find security vulnerabilities in code'); + + const metadataSection = result.content.sections.find(s => s.type === 'metadata'); + if (metadataSection?.type === 'metadata') { + expect(metadataSection.data.codexAgent?.name).toBe('Security Reviewer'); + expect(metadataSection.data.codexAgent?.description).toBe('Find security vulnerabilities in code'); + } + }); + + it('should parse nickname_candidates array', () => { + const content = ` +name = "Reviewer" +description = "Reviews code." +developer_instructions = "Review code." +nickname_candidates = ["sec-review", "security"] +`.trim(); + + const result = fromCodexAgentRole(content, baseMetadata); + const metadataSection = result.content.sections.find(s => s.type === 'metadata'); + + if (metadataSection?.type === 'metadata') { + expect(metadataSection.data.codexAgent?.nicknameCandidates).toEqual(['sec-review', 'security']); + } + }); + + it('should parse mcp_servers configuration', () => { + const content = ` +name = "MCP Agent" +description = "Agent with MCP servers." +developer_instructions = "Use MCP tools." + +[mcp_servers.github] +command = "npx" +args = ["-y", "@modelcontextprotocol/server-github"] +`.trim(); + + const result = fromCodexAgentRole(content, baseMetadata); + const metadataSection = result.content.sections.find(s => s.type === 'metadata'); + + if (metadataSection?.type === 'metadata') { + expect(metadataSection.data.codexAgent?.mcpServers).toBeDefined(); + expect(metadataSection.data.codexAgent?.mcpServers?.github?.command).toBe('npx'); + expect(metadataSection.data.codexAgent?.mcpServers?.github?.args).toEqual(['-y', '@modelcontextprotocol/server-github']); + } + }); + + it('should parse skills.config', () => { + const content = ` +name = "Skilled Agent" +description = "Agent with skills config." +developer_instructions = "Use skills." + +[skills.config] +focus_areas = "security,auth" +`.trim(); + + const result = fromCodexAgentRole(content, baseMetadata); + const metadataSection = result.content.sections.find(s => s.type === 'metadata'); + + if (metadataSection?.type === 'metadata') { + expect(metadataSection.data.codexAgent?.skillsConfig).toBeDefined(); + expect(metadataSection.data.codexAgent?.skillsConfig?.focus_areas).toBe('security,auth'); + } + }); + + it('should parse a full subagent with all fields', () => { + const content = ` +name = "Security Auditor" +description = "Deep security analysis of code changes" +nickname_candidates = ["sec-audit", "security-check"] +model = "o3" +model_reasoning_effort = "high" +sandbox_mode = "read-only" +developer_instructions = "Focus on OWASP Top 10 vulnerabilities." + +[mcp_servers.semgrep] +command = "npx" +args = ["-y", "semgrep-mcp-server"] + +[skills.config] +severity_threshold = "medium" +`.trim(); + + const result = fromCodexAgentRole(content, baseMetadata); + + expect(result.name).toBe('Security Auditor'); + expect(result.description).toBe('Deep security analysis of code changes'); + + const metadataSection = result.content.sections.find(s => s.type === 'metadata'); + if (metadataSection?.type === 'metadata') { + const ca = metadataSection.data.codexAgent; + expect(ca?.name).toBe('Security Auditor'); + expect(ca?.description).toBe('Deep security analysis of code changes'); + expect(ca?.nicknameCandidates).toEqual(['sec-audit', 'security-check']); + expect(ca?.model).toBe('o3'); + expect(ca?.modelReasoningEffort).toBe('high'); + expect(ca?.sandboxMode).toBe('read-only'); + expect(ca?.mcpServers?.semgrep?.command).toBe('npx'); + expect(ca?.skillsConfig?.severity_threshold).toBe('medium'); + } + }); + }); + describe('roundtrip metadata', () => { it('should preserve all codexAgent fields for roundtrip', () => { const content = ` diff --git a/packages/converters/src/__tests__/to-codex.test.ts b/packages/converters/src/__tests__/to-codex.test.ts index c9ffff31..b17f3676 100644 --- a/packages/converters/src/__tests__/to-codex.test.ts +++ b/packages/converters/src/__tests__/to-codex.test.ts @@ -887,6 +887,193 @@ Old content for my-command expect(result.lossyConversion).toBe(true); }); + it('should include name and description in TOML output', () => { + const agentPkg: CanonicalPackage = { + ...minimalCanonicalPackage, + name: 'security-reviewer', + subtype: 'agent', + description: 'Find security issues.', + content: { + format: 'canonical', + version: '1.0', + sections: [ + { + type: 'metadata', + data: { + title: 'Security Reviewer', + description: 'Find security issues.', + codexAgent: { name: 'Security Reviewer', description: 'Find security issues in code changes' }, + }, + }, + { type: 'instructions', title: 'Instructions', content: 'Focus on OWASP.' }, + ], + }, + }; + + const result = toCodex(agentPkg); + + expect(result.content).toContain('name = '); + expect(result.content).toContain('Security Reviewer'); + expect(result.content).toContain('description = '); + expect(result.content).toContain('Find security issues in code changes'); + }); + + it('should include nickname_candidates in TOML output', () => { + const agentPkg: CanonicalPackage = { + ...minimalCanonicalPackage, + name: 'reviewer', + subtype: 'agent', + description: 'Review code.', + content: { + format: 'canonical', + version: '1.0', + sections: [ + { + type: 'metadata', + data: { + title: 'Reviewer', + description: 'Review code.', + codexAgent: { + name: 'Reviewer', + description: 'Review code.', + nicknameCandidates: ['rev', 'code-review'], + }, + }, + }, + { type: 'instructions', title: 'Instructions', content: 'Review.' }, + ], + }, + }; + + const result = toCodex(agentPkg); + + expect(result.content).toContain('nickname_candidates'); + expect(result.content).toContain('rev'); + expect(result.content).toContain('code-review'); + }); + + it('should include mcp_servers in TOML output', () => { + const agentPkg: CanonicalPackage = { + ...minimalCanonicalPackage, + name: 'mcp-agent', + subtype: 'agent', + description: 'Agent with MCP.', + content: { + format: 'canonical', + version: '1.0', + sections: [ + { + type: 'metadata', + data: { + title: 'MCP Agent', + description: 'Agent with MCP.', + codexAgent: { + name: 'MCP Agent', + description: 'Agent with MCP.', + mcpServers: { + github: { command: 'npx', args: ['-y', '@mcp/server-github'] }, + }, + }, + }, + }, + { type: 'instructions', title: 'Instructions', content: 'Use MCP.' }, + ], + }, + }; + + const result = toCodex(agentPkg); + + expect(result.content).toContain('mcp_servers'); + expect(result.content).toContain('github'); + expect(result.content).toContain('npx'); + }); + + it('should include skills.config in TOML output', () => { + const agentPkg: CanonicalPackage = { + ...minimalCanonicalPackage, + name: 'skilled-agent', + subtype: 'agent', + description: 'Agent with skills.', + content: { + format: 'canonical', + version: '1.0', + sections: [ + { + type: 'metadata', + data: { + title: 'Skilled Agent', + description: 'Agent with skills.', + codexAgent: { + name: 'Skilled Agent', + description: 'Agent with skills.', + skillsConfig: { focus: 'security' }, + }, + }, + }, + { type: 'instructions', title: 'Instructions', content: 'Use skills.' }, + ], + }, + }; + + const result = toCodex(agentPkg); + + expect(result.content).toContain('skills'); + expect(result.content).toContain('config'); + expect(result.content).toContain('security'); + }); + + it('should roundtrip full subagent with all new fields', () => { + const agentPkg: CanonicalPackage = { + ...minimalCanonicalPackage, + name: 'full-subagent', + subtype: 'agent', + description: 'Full subagent.', + content: { + format: 'canonical', + version: '1.0', + sections: [ + { + type: 'metadata', + data: { + title: 'Full Subagent', + description: 'Full subagent.', + codexAgent: { + name: 'Full Subagent', + description: 'Full subagent with all fields.', + nicknameCandidates: ['full', 'sub'], + model: 'o3', + modelReasoningEffort: 'high', + sandboxMode: 'read-only', + mcpServers: { test: { command: 'echo', args: ['hello'] } }, + skillsConfig: { key: 'value' }, + }, + }, + }, + { type: 'instructions', title: 'Instructions', content: 'Do everything.' }, + ], + }, + }; + + const tomlOutput = toCodex(agentPkg).content; + const reparsed = fromCodexAgentRole(tomlOutput, { + id: 'full-subagent', name: 'full-subagent', version: '1.0.0', author: 'test', + }); + + const meta = reparsed.content.sections.find(s => s.type === 'metadata'); + if (meta?.type === 'metadata') { + expect(meta.data.codexAgent?.name).toBe('Full Subagent'); + expect(meta.data.codexAgent?.description).toBe('Full subagent with all fields.'); + expect(meta.data.codexAgent?.nicknameCandidates).toEqual(['full', 'sub']); + expect(meta.data.codexAgent?.model).toBe('o3'); + expect(meta.data.codexAgent?.modelReasoningEffort).toBe('high'); + expect(meta.data.codexAgent?.sandboxMode).toBe('read-only'); + expect(meta.data.codexAgent?.mcpServers?.test?.command).toBe('echo'); + expect(meta.data.codexAgent?.skillsConfig?.key).toBe('value'); + } + const instr = reparsed.content.sections.find(s => s.type === 'instructions'); + expect(instr?.type === 'instructions' && instr.content).toContain('Do everything.'); + }); + it('should produce a roundtrip-stable agent role', () => { const agentPkg: CanonicalPackage = { ...minimalCanonicalPackage, diff --git a/packages/converters/src/from-codex.ts b/packages/converters/src/from-codex.ts index 1b19e333..cf6257f5 100644 --- a/packages/converters/src/from-codex.ts +++ b/packages/converters/src/from-codex.ts @@ -179,10 +179,15 @@ export function fromCodex( * @see https://developers.openai.com/codex/multi-agent/#agent-roles */ interface CodexAgentRoleToml { + name?: string; + description?: string; + developer_instructions?: string; + nickname_candidates?: string[]; model?: string; model_reasoning_effort?: 'low' | 'medium' | 'high'; - developer_instructions?: string; sandbox_mode?: 'read-only' | 'workspace-write' | 'danger-full-access'; + mcp_servers?: Record }>; + skills?: { config?: Record; [key: string]: unknown }; } /** @@ -212,8 +217,8 @@ export function fromCodexAgentRole( const metadataSection: MetadataSection = { type: 'metadata', data: { - title: metadata.name || metadata.id, - description: metadata.description || '', + title: role.name || metadata.name || metadata.id, + description: role.description || metadata.description || '', version: metadata.version || '1.0.0', author: metadata.author, }, @@ -224,6 +229,11 @@ export function fromCodexAgentRole( model: role.model, modelReasoningEffort: role.model_reasoning_effort, sandboxMode: role.sandbox_mode, + name: role.name, + description: role.description, + nicknameCandidates: role.nickname_candidates, + mcpServers: role.mcp_servers as Record }>, + skillsConfig: role.skills?.config as Record, }; sections.push(metadataSection); @@ -246,10 +256,10 @@ export function fromCodexAgentRole( const pkg: CanonicalPackage = { ...metadata, id: metadata.id, - name: metadata.name || metadata.id, + name: role.name || metadata.name || metadata.id, version: metadata.version, author: metadata.author, - description: metadata.description || '', + description: role.description || metadata.description || '', tags: metadata.tags || [], format: 'codex', subtype: 'agent', diff --git a/packages/converters/src/to-codex.ts b/packages/converters/src/to-codex.ts index f7d54a1e..6a7ea70f 100644 --- a/packages/converters/src/to-codex.ts +++ b/packages/converters/src/to-codex.ts @@ -129,6 +129,27 @@ function convertToAgentRoleToml( ? metadataSection.data.codexAgent : undefined; + // Required subagent fields: name, description, developer_instructions + const agentName = codexAgent?.name || pkg.name; + role['name'] = agentName; + + const agentDescription = codexAgent?.description + || (metadataSection?.type === 'metadata' ? metadataSection.data.description : '') + || pkg.description || ''; + role['description'] = agentDescription; + + // Map instructions section to developer_instructions + if (instructionsSection?.type === 'instructions') { + role['developer_instructions'] = instructionsSection.content; + } else if (pkg.description) { + // Fall back to package description + role['developer_instructions'] = pkg.description; + } + + // Optional subagent fields + if (codexAgent?.nicknameCandidates && codexAgent.nicknameCandidates.length > 0) { + role['nickname_candidates'] = codexAgent.nicknameCandidates; + } if (codexAgent?.model) { role['model'] = codexAgent.model; } @@ -138,13 +159,11 @@ function convertToAgentRoleToml( if (codexAgent?.sandboxMode) { role['sandbox_mode'] = codexAgent.sandboxMode; } - - // Map instructions section to developer_instructions - if (instructionsSection?.type === 'instructions') { - role['developer_instructions'] = instructionsSection.content; - } else if (pkg.description) { - // Fall back to package description - role['developer_instructions'] = pkg.description; + if (codexAgent?.mcpServers && Object.keys(codexAgent.mcpServers).length > 0) { + role['mcp_servers'] = codexAgent.mcpServers; + } + if (codexAgent?.skillsConfig && Object.keys(codexAgent.skillsConfig).length > 0) { + role['skills'] = { config: codexAgent.skillsConfig }; } // Warn about unsupported sections diff --git a/packages/converters/src/types/canonical.ts b/packages/converters/src/types/canonical.ts index eeadfa01..b942d007 100644 --- a/packages/converters/src/types/canonical.ts +++ b/packages/converters/src/types/canonical.ts @@ -156,6 +156,11 @@ export interface CanonicalPackage { model?: string; // Model to use for this agent role modelReasoningEffort?: 'low' | 'medium' | 'high'; // Reasoning effort level sandboxMode?: 'read-only' | 'workspace-write' | 'danger-full-access'; // Sandbox isolation mode + name?: string; // Display name of the subagent + description?: string; // Short description of what this agent does + nicknameCandidates?: string[]; // Alternative names for the agent + mcpServers?: Record }>; // MCP server configs + skillsConfig?: Record; // Skills configuration }; droid?: { argumentHint?: string; // Usage hint for slash commands @@ -302,6 +307,11 @@ export interface MetadataSection { model?: string; // Model to use for this agent role modelReasoningEffort?: 'low' | 'medium' | 'high'; // Reasoning effort level sandboxMode?: 'read-only' | 'workspace-write' | 'danger-full-access'; // Sandbox isolation mode + name?: string; // Display name of the subagent + description?: string; // Short description of what this agent does + nicknameCandidates?: string[]; // Alternative names for the agent + mcpServers?: Record }>; // MCP server configs + skillsConfig?: Record; // Skills configuration }; opencodeSlashCommand?: { description?: string; // Description of the slash command