From 2d4c7899ddf6741842543266be9f0949d1a1eab8 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Tue, 24 Mar 2026 10:46:59 -0700 Subject: [PATCH] Update provider configs from official docs, add HARNESSES.md reference Research each harness's official documentation to verify and correct provider frontmatter configs. Remove Codex/Gemini body transforms that targeted their commands systems, not skills. - Add compatibility + metadata to Cursor and Agents (Copilot) - Add allowed-tools to Pi - Remove Codex $ARGNAME and Gemini {{args}} body transforms - Add HARNESSES.md as source of truth for harness capabilities Co-Authored-By: Claude Opus 4.6 (1M context) --- .agents/skills/frontend-design/SKILL.md | 1 + HARNESSES.md | 81 ++++++++++++++++ scripts/lib/transformers/providers.js | 18 +--- tests/build.test.js | 4 +- tests/lib/transformers/providers.test.js | 112 ----------------------- 5 files changed, 87 insertions(+), 129 deletions(-) create mode 100644 HARNESSES.md diff --git a/.agents/skills/frontend-design/SKILL.md b/.agents/skills/frontend-design/SKILL.md index 87f4a1516..367b4d4d4 100644 --- a/.agents/skills/frontend-design/SKILL.md +++ b/.agents/skills/frontend-design/SKILL.md @@ -1,6 +1,7 @@ --- name: frontend-design description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications. Generates creative, polished code that avoids generic AI aesthetics. +license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution. --- This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices. diff --git a/HARNESSES.md b/HARNESSES.md new file mode 100644 index 000000000..f9b6dec69 --- /dev/null +++ b/HARNESSES.md @@ -0,0 +1,81 @@ +# Harness Skills Capabilities Reference + +Source of truth for what each AI coding harness supports in terms of agent skills. +Used to inform provider configs in `scripts/lib/transformers/providers.js`. + +Last verified: 2026-03-24 + +## Official Documentation + +| Harness | Docs URL | +|---------|----------| +| Claude Code | https://code.claude.com/docs/en/skills | +| Cursor | https://cursor.com/docs/context/skills | +| Gemini CLI | https://geminicli.com/docs/cli/skills/ | +| Codex CLI | https://developers.openai.com/codex/skills | +| GitHub Copilot (Agents) | https://code.visualstudio.com/docs/copilot/customization/agent-skills | +| Kiro | https://kiro.dev/docs/skills/ | +| OpenCode | https://opencode.ai/docs/skills/ | +| Pi | https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/skills.md | + +## Spec Compliance + +All harnesses follow the [Agent Skills specification](https://agentskills.io/specification) to varying degrees. The spec defines these frontmatter fields: `name`, `description`, `license`, `compatibility`, `metadata`, `allowed-tools`. + +Provider-specific extensions beyond the spec: `user-invocable`, `argument-hint`, `disable-model-invocation`, `allowed-tools` (extended syntax), `model`, `effort`, `context`, `agent`, `hooks`, `subtask`, `mcp`. + +## Frontmatter Support + +Fields marked with * are spec-standard. Others are provider extensions. + +| Field | Claude Code | Cursor | Gemini | Codex | Copilot | Kiro | OpenCode | Pi | +|-------|:-----------:|:------:|:------:|:-----:|:-------:|:----:|:--------:|:--:| +| `name`* | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| `description`* | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| `license`* | Yes | Yes | Ignored | No | Yes | Yes | Yes | Yes | +| `compatibility`* | Yes | Yes | Ignored | No | Yes | Yes | Yes | Yes | +| `metadata`* | Yes | Yes | Ignored | No | Yes | Yes | Yes | Yes | +| `allowed-tools`* | Yes | No | Ignored | No | No | No | Yes | Yes | +| `user-invocable` | Yes | No | No | No | Yes | No | Yes | No | +| `argument-hint` | Yes | No | No | No | Yes | No | Yes | No | +| `disable-model-invocation` | Yes | Yes | No | No | Yes | No | Yes | Yes | +| `model` | Yes | No | No | No | No | No | Yes | No | +| `effort` | Yes | No | No | No | No | No | No | No | +| `context` | Yes | No | No | No | No | No | No | No | +| `agent` | Yes | No | No | No | No | No | Yes | No | +| `hooks` | Yes | No | No | No | No | No | No | No | + +Notes: +- Gemini CLI validates only `name` and `description`; other spec fields are parsed but ignored. +- Codex CLI uses a separate `agents/openai.yaml` sidecar for extended metadata (icons, branding, MCP tools, invocation control). +- Kiro recognizes `user-invocable` and `disable-model-invocation` per community reports but does not formally document them. +- Unknown fields are silently ignored by all harnesses. + +## Skill Directory Structure + +| Harness | Native directory | Also reads | +|---------|-----------------|------------| +| Claude Code | `.claude/skills/` | - | +| Cursor | `.cursor/skills/` | `.agents/skills/`, `.claude/skills/`, `.codex/skills/` | +| Gemini CLI | `.gemini/skills/` | `.agents/skills/` | +| Codex CLI | `.agents/skills/` (primary) | - | +| GitHub Copilot | `.github/skills/` | `.agents/skills/`, `.claude/skills/` | +| Kiro | `.kiro/skills/` | - | +| OpenCode | `.opencode/skills/` | `.agents/skills/`, `.claude/skills/` | +| Pi | `.pi/skills/` | `.agents/skills/` | + +All harnesses support the `{skill-name}/SKILL.md` directory structure with optional `reference/`, `scripts/`, and `assets/` subdirectories. + +## Placeholder / Variable Substitution + +Claude Code supports runtime variable substitution directly in SKILL.md bodies: `$ARGUMENTS`, `$0`-`$N`, `${CLAUDE_SKILL_DIR}`, `${CLAUDE_SESSION_ID}`. No other harness supports substitution in skills. + +Some harnesses have separate "custom commands" systems (distinct from skills) with their own substitution: + +| Harness | Command system | Substitution syntax | +|---------|---------------|-------------------| +| Gemini CLI | `.gemini/commands/` (TOML) | `{{args}}`, `!{shell}`, `@{file}` | +| Codex CLI | `.codex/prompts/` | `$ARGNAME` | +| OpenCode | `.opencode/commands/` | `$ARGUMENTS`, `$1`-`$N`, `` !`shell` `` | + +Our build system handles cross-provider placeholders at compile time via `replacePlaceholders()` for `{{model}}`, `{{config_file}}`, `{{ask_instruction}}`, and `{{available_commands}}`. diff --git a/scripts/lib/transformers/providers.js b/scripts/lib/transformers/providers.js index d8afc7c07..38595b5aa 100644 --- a/scripts/lib/transformers/providers.js +++ b/scripts/lib/transformers/providers.js @@ -13,7 +13,7 @@ export const PROVIDERS = { provider: 'cursor', configDir: '.cursor', displayName: 'Cursor', - frontmatterFields: ['license'], + frontmatterFields: ['license', 'compatibility', 'metadata'], }, 'claude-code': { provider: 'claude-code', @@ -26,30 +26,18 @@ export const PROVIDERS = { configDir: '.gemini', displayName: 'Gemini', frontmatterFields: [], - bodyTransform: (body, skill) => { - if (skill.userInvocable) { - return body.replace(/\{\{[^}]+\}\}/g, '{{args}}'); - } - return body; - }, }, codex: { provider: 'codex', configDir: '.codex', displayName: 'Codex', frontmatterFields: ['argument-hint', 'license'], - bodyTransform: (body, skill) => { - if (skill.userInvocable) { - return body.replace(/\{\{([^}]+)\}\}/g, (_, argName) => `$${argName.toUpperCase()}`); - } - return body; - }, }, agents: { provider: 'agents', configDir: '.agents', displayName: 'Agents', - frontmatterFields: ['user-invocable', 'argument-hint'], + frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata'], }, kiro: { provider: 'kiro', @@ -67,6 +55,6 @@ export const PROVIDERS = { provider: 'pi', configDir: '.pi', displayName: 'Pi', - frontmatterFields: ['license', 'compatibility', 'metadata'], + frontmatterFields: ['license', 'compatibility', 'metadata', 'allowed-tools'], }, }; diff --git a/tests/build.test.js b/tests/build.test.js index 78a831f83..7a1872262 100644 --- a/tests/build.test.js +++ b/tests/build.test.js @@ -203,13 +203,13 @@ Please audit {{target}} for technical quality. Ask {{model}} for help.`; // Verify Gemini: skill in skills directory expect(fs.existsSync(path.join(DIST_DIR, 'gemini/.gemini/skills/audit/SKILL.md'))).toBe(true); const geminiContent = fs.readFileSync(path.join(DIST_DIR, 'gemini/.gemini/skills/audit/SKILL.md'), 'utf-8'); - expect(geminiContent).toContain('{{args}}'); // Replaced for user-invocable in Gemini + expect(geminiContent).toContain('{{target}}'); // No body transform, placeholder preserved expect(geminiContent).toContain('Gemini'); // Verify Codex: skill in skills directory expect(fs.existsSync(path.join(DIST_DIR, 'codex/.codex/skills/audit/SKILL.md'))).toBe(true); const codexContent = fs.readFileSync(path.join(DIST_DIR, 'codex/.codex/skills/audit/SKILL.md'), 'utf-8'); - expect(codexContent).toContain('$TARGET'); // Replaced for user-invocable in Codex + expect(codexContent).toContain('{{target}}'); // No body transform, placeholder preserved expect(codexContent).toContain('GPT'); }); diff --git a/tests/lib/transformers/providers.test.js b/tests/lib/transformers/providers.test.js index 67b278856..5d38b3810 100644 --- a/tests/lib/transformers/providers.test.js +++ b/tests/lib/transformers/providers.test.js @@ -190,115 +190,3 @@ for (const [key, config] of Object.entries(PROVIDERS)) { } }); } - -// Provider-specific body transform tests -describe('Codex body transform', () => { - const transform = createTransformer(PROVIDERS.codex); - - beforeEach(() => { - if (fs.existsSync(TEST_DIR)) fs.rmSync(TEST_DIR, { recursive: true, force: true }); - }); - afterEach(() => { - if (fs.existsSync(TEST_DIR)) fs.rmSync(TEST_DIR, { recursive: true, force: true }); - }); - - test('should transform {{argname}} to $ARGNAME for user-invocable skills', () => { - const skills = [{ - name: 'test', - description: 'Test', - userInvocable: true, - argumentHint: '[target]', - body: 'Process {{target}} now.' - }]; - transform(skills, TEST_DIR); - const content = fs.readFileSync( - path.join(TEST_DIR, `codex/.codex/skills/test/SKILL.md`), 'utf-8' - ); - expect(content).toContain('Process $TARGET now.'); - }); - - test('should not transform {{argname}} for non-user-invocable skills', () => { - const skills = [{ - name: 'test', - description: 'Test', - userInvocable: false, - body: 'Process {{target}} now.' - }]; - transform(skills, TEST_DIR); - const content = fs.readFileSync( - path.join(TEST_DIR, `codex/.codex/skills/test/SKILL.md`), 'utf-8' - ); - // {{target}} should have been left as-is (not transformed by replacePlaceholders either, - // since 'target' is not a known placeholder) - expect(content).toContain('Process {{target}} now.'); - }); - - test('should replace system placeholders before body transform', () => { - const skills = [{ - name: 'test', - description: 'Test', - userInvocable: true, - body: 'Ask {{model}} about {{target}}.' - }]; - transform(skills, TEST_DIR); - const content = fs.readFileSync( - path.join(TEST_DIR, `codex/.codex/skills/test/SKILL.md`), 'utf-8' - ); - // {{model}} replaced first by replacePlaceholders, then {{target}} by bodyTransform - expect(content).toContain('Ask GPT about $TARGET.'); - }); -}); - -describe('Gemini body transform', () => { - const transform = createTransformer(PROVIDERS.gemini); - - beforeEach(() => { - if (fs.existsSync(TEST_DIR)) fs.rmSync(TEST_DIR, { recursive: true, force: true }); - }); - afterEach(() => { - if (fs.existsSync(TEST_DIR)) fs.rmSync(TEST_DIR, { recursive: true, force: true }); - }); - - test('should transform all remaining {{*}} to {{args}} for user-invocable skills', () => { - const skills = [{ - name: 'test', - description: 'Test', - userInvocable: true, - body: 'Process {{target}} and {{format}}.' - }]; - transform(skills, TEST_DIR); - const content = fs.readFileSync( - path.join(TEST_DIR, `gemini/.gemini/skills/test/SKILL.md`), 'utf-8' - ); - expect(content).toContain('Process {{args}} and {{args}}.'); - }); - - test('should not transform for non-user-invocable skills', () => { - const skills = [{ - name: 'test', - description: 'Test', - userInvocable: false, - body: 'Process {{target}}.' - }]; - transform(skills, TEST_DIR); - const content = fs.readFileSync( - path.join(TEST_DIR, `gemini/.gemini/skills/test/SKILL.md`), 'utf-8' - ); - expect(content).toContain('Process {{target}}.'); - }); - - test('should replace system placeholders before body transform', () => { - const skills = [{ - name: 'test', - description: 'Test', - userInvocable: true, - body: 'Ask {{model}} about {{target}}.' - }]; - transform(skills, TEST_DIR); - const content = fs.readFileSync( - path.join(TEST_DIR, `gemini/.gemini/skills/test/SKILL.md`), 'utf-8' - ); - // {{model}} replaced first, then remaining {{target}} becomes {{args}} - expect(content).toContain('Ask Gemini about {{args}}.'); - }); -});