# Plan: Phase A — Core (Multi-Platform Expansion: Codex CLI + Cursor)

**Date:** 2026-04-01
**Spec:** docs/changes/multi-platform-expansion/proposal.md
**Estimated tasks:** 9
**Estimated time:** 40–50 minutes

---

## Goal

Add Codex CLI and Cursor as first-class harness platforms by extending the platform schema, creating skill directory symlinks, implementing two thin renderers, wiring them into the generator, and adding tests — leaving existing claude-code and gemini-cli generation unaffected.

---

## Observable Truths (Acceptance Criteria)

1. When `harness generate-slash-commands --platform cursor` runs, the system shall generate `.mdc` files at `agents/commands/cursor/harness/<skill>.mdc` for all 80 skills.
2. When `harness generate-slash-commands --platform codex` runs, the system shall generate `agents/commands/codex/harness/<skill>/SKILL.md` and `agents/commands/codex/harness/<skill>/agents/openai.yaml` for all 80 skills.
3. When `harness generate-slash-commands --platform codex` runs, the system shall generate a top-level `agents/commands/codex/AGENTS.md` listing all available skills.
4. The system shall accept `codex:` and `cursor:` optional blocks in `skill.yaml` without Zod validation errors.
5. When `cursor.alwaysApply: true` is set in skill.yaml, the rendered `.mdc` frontmatter shall include `alwaysApply: true`.
6. When `cursor.globs` is set in skill.yaml, the rendered `.mdc` frontmatter shall include the glob array.
7. `agents/skills/codex/` shall contain symlinks for all 80 skills pointing to `../claude-code/<skill>`.
8. `agents/skills/cursor/` shall contain symlinks for all 80 skills pointing to `../claude-code/<skill>`.
9. The system shall pass all platform parity tests after symlinks are created.
10. The system shall pass `harness validate` after all changes are applied.
11. When `harness generate-slash-commands --platform claude-code` or `--platform gemini-cli` runs, the system shall produce identical output as before these changes.

---

## File Map

```
MODIFY packages/cli/src/slash-commands/types.ts       (+codex, +cursor to VALID_PLATFORMS; +generated headers)
MODIFY packages/cli/src/skill/schema.ts               (+codex, +cursor to ALLOWED_PLATFORMS; +cursor/codex optional Zod blocks)
MODIFY agents/skills/tests/schema.ts                  (+codex, +cursor to ALLOWED_PLATFORMS)
CREATE packages/cli/src/slash-commands/render-cursor.ts
CREATE packages/cli/src/slash-commands/render-codex.ts
MODIFY packages/cli/src/commands/generate-slash-commands.ts  (+codex/cursor cases to dispatch; +resolveOutputDir; +fileExtension)
CREATE packages/cli/tests/slash-commands/render-cursor.test.ts
CREATE packages/cli/tests/slash-commands/render-codex.test.ts
CREATE agents/skills/codex/                           (80 symlinks → ../claude-code/<skill>)
CREATE agents/skills/cursor/                          (80 symlinks → ../claude-code/<skill>)
```

---

## Tasks

### Task 1: Extend platform types and schema

**Depends on:** none
**Files:** `packages/cli/src/slash-commands/types.ts`, `packages/cli/src/skill/schema.ts`, `agents/skills/tests/schema.ts`

1. Open `packages/cli/src/slash-commands/types.ts`. Change:

   ```typescript
   export type Platform = 'claude-code' | 'gemini-cli';
   export const VALID_PLATFORMS: Platform[] = ['claude-code', 'gemini-cli'];
   ```

   to:

   ```typescript
   export type Platform = 'claude-code' | 'gemini-cli' | 'codex' | 'cursor';
   export const VALID_PLATFORMS: Platform[] = ['claude-code', 'gemini-cli', 'codex', 'cursor'];
   ```

   Also add two new generated header constants after the existing ones:

   ```typescript
   export const GENERATED_HEADER_CURSOR =
     '<!-- Generated by harness generate-slash-commands. Do not edit. -->';
   export const GENERATED_HEADER_CODEX =
     '<!-- Generated by harness generate-slash-commands. Do not edit. -->';
   ```

2. Open `packages/cli/src/skill/schema.ts`. Change line 46:

   ```typescript
   const ALLOWED_PLATFORMS = ['claude-code', 'gemini-cli'] as const;
   ```

   to:

   ```typescript
   const ALLOWED_PLATFORMS = ['claude-code', 'gemini-cli', 'codex', 'cursor'] as const;
   ```

   Add two new Zod sub-schemas before `SkillMetadataSchema` (after `SkillStateSchema`):

   ```typescript
   const SkillCursorSchema = z.object({
     globs: z.array(z.string()).optional(),
     alwaysApply: z.boolean().default(false),
   });

   const SkillCodexSchema = z.object({
     instructions_override: z.string().optional(),
   });
   ```

   Then inside `SkillMetadataSchema`, add two optional fields after `stack_signals`:

   ```typescript
   cursor: SkillCursorSchema.optional(),
   codex: SkillCodexSchema.optional(),
   ```

   Export the new types at the bottom alongside existing types:

   ```typescript
   export type SkillCursor = z.infer<typeof SkillCursorSchema>;
   export type SkillCodex = z.infer<typeof SkillCodexSchema>;
   ```

3. Open `agents/skills/tests/schema.ts`. Change:

   ```typescript
   const ALLOWED_PLATFORMS = ['claude-code', 'gemini-cli'] as const;
   ```

   to:

   ```typescript
   const ALLOWED_PLATFORMS = ['claude-code', 'gemini-cli', 'codex', 'cursor'] as const;
   ```

4. Run: `npx vitest run packages/cli/tests/skill/schema.test.ts`
5. Observe: all existing tests pass (no breakage).
6. Run: `harness validate`
7. Commit: `feat(platforms): extend ALLOWED_PLATFORMS and skill.yaml schema for codex and cursor`

---

### Task 2: Create `render-cursor.ts` (TDD)

**Depends on:** Task 1
**Files:** `packages/cli/src/slash-commands/render-cursor.ts`, `packages/cli/tests/slash-commands/render-cursor.test.ts`

1. Create test file `packages/cli/tests/slash-commands/render-cursor.test.ts`:

   ```typescript
   import { describe, it, expect } from 'vitest';
   import { renderCursor } from '../../src/slash-commands/render-cursor';
   import { GENERATED_HEADER_CURSOR } from '../../src/slash-commands/types';
   import type { SlashCommandSpec } from '../../src/slash-commands/types';

   const baseSpec: SlashCommandSpec = {
     name: 'brainstorming',
     namespace: 'harness',
     fullName: 'harness:brainstorming',
     description: 'Structured ideation and exploration with harness methodology',
     version: '1.0.0',
     cognitiveMode: 'constructive-architect',
     tools: ['Read', 'Write', 'Bash'],
     args: [{ name: 'path', description: 'Project path', required: false }],
     skillYamlName: 'harness-brainstorming',
     sourceDir: 'harness-brainstorming',
     skillsBaseDir: 'agents/skills/claude-code',
     prompt: {
       context: 'Cognitive mode: constructive-architect\nType: rigid',
       objective: 'Structured ideation and exploration',
       executionContext: '@agents/skills/claude-code/harness-brainstorming/SKILL.md',
       process: '1. Read SKILL.md and follow its workflow directly',
     },
   };

   const skillMdContent = '# Harness Brainstorming\n\nExplore ideas systematically.';

   describe('renderCursor', () => {
     it('outputs valid YAML frontmatter with --- delimiters', () => {
       const output = renderCursor(baseSpec, skillMdContent);
       expect(output.startsWith('---\n')).toBe(true);
       expect(output).toContain('\n---\n');
     });

     it('includes description in frontmatter', () => {
       const output = renderCursor(baseSpec, skillMdContent);
       expect(output).toContain(
         'description: Structured ideation and exploration with harness methodology'
       );
     });

     it('defaults alwaysApply to false when no cursor config', () => {
       const output = renderCursor(baseSpec, skillMdContent);
       expect(output).toContain('alwaysApply: false');
     });

     it('sets alwaysApply: true when cursor.alwaysApply is true', () => {
       const output = renderCursor(baseSpec, skillMdContent, { alwaysApply: true });
       expect(output).toContain('alwaysApply: true');
     });

     it('includes globs in frontmatter when cursor.globs is provided', () => {
       const output = renderCursor(baseSpec, skillMdContent, { globs: ['src/**/*.ts'] });
       expect(output).toContain('globs:');
       expect(output).toContain('src/**/*.ts');
     });

     it('omits globs when cursor config has no globs', () => {
       const output = renderCursor(baseSpec, skillMdContent);
       expect(output).not.toContain('globs:');
     });

     it('includes SKILL.md content after frontmatter', () => {
       const output = renderCursor(baseSpec, skillMdContent);
       expect(output).toContain('# Harness Brainstorming');
       expect(output).toContain('Explore ideas systematically.');
     });

     it('includes the generated header comment after frontmatter', () => {
       const output = renderCursor(baseSpec, skillMdContent);
       expect(output).toContain(GENERATED_HEADER_CURSOR);
     });
   });
   ```

2. Run: `npx vitest run packages/cli/tests/slash-commands/render-cursor.test.ts`
3. Observe failure: `Cannot find module '../../src/slash-commands/render-cursor'`
4. Create `packages/cli/src/slash-commands/render-cursor.ts`:

   ```typescript
   import type { SlashCommandSpec } from './types';
   import { GENERATED_HEADER_CURSOR } from './types';

   export interface CursorConfig {
     globs?: string[];
     alwaysApply?: boolean;
   }

   export function renderCursor(
     spec: SlashCommandSpec,
     skillMdContent: string,
     cursorConfig?: CursorConfig
   ): string {
     const lines: string[] = ['---'];
     lines.push(`description: ${spec.description}`);

     if (cursorConfig?.globs && cursorConfig.globs.length > 0) {
       lines.push('globs:');
       for (const glob of cursorConfig.globs) {
         lines.push(`  - ${glob}`);
       }
     }

     const alwaysApply = cursorConfig?.alwaysApply ?? false;
     lines.push(`alwaysApply: ${alwaysApply}`);
     lines.push('---');
     lines.push('');
     lines.push(GENERATED_HEADER_CURSOR);
     lines.push('');
     lines.push(skillMdContent);
     lines.push('');

     return lines.join('\n');
   }
   ```

5. Run: `npx vitest run packages/cli/tests/slash-commands/render-cursor.test.ts`
6. Observe: all 8 tests pass
7. Run: `harness validate`
8. Commit: `feat(cursor): implement render-cursor.ts with frontmatter and SKILL.md content`

---

### Task 3: Create `render-codex.ts` (TDD)

**Depends on:** Task 1
**Files:** `packages/cli/src/slash-commands/render-codex.ts`, `packages/cli/tests/slash-commands/render-codex.test.ts`

1. Create test file `packages/cli/tests/slash-commands/render-codex.test.ts`:

   ```typescript
   import { describe, it, expect } from 'vitest';
   import {
     renderCodexSkill,
     renderCodexOpenaiYaml,
     renderCodexAgentsMd,
   } from '../../src/slash-commands/render-codex';
   import type { SlashCommandSpec } from '../../src/slash-commands/types';

   const baseSpec: SlashCommandSpec = {
     name: 'brainstorming',
     namespace: 'harness',
     fullName: 'harness:brainstorming',
     description: 'Structured ideation and exploration with harness methodology',
     version: '1.0.0',
     cognitiveMode: 'constructive-architect',
     tools: ['Read', 'Write', 'Bash'],
     args: [{ name: 'path', description: 'Project path', required: false }],
     skillYamlName: 'harness-brainstorming',
     sourceDir: 'harness-brainstorming',
     skillsBaseDir: 'agents/skills/claude-code',
     prompt: {
       context: 'Cognitive mode: constructive-architect\nType: rigid',
       objective: 'Structured ideation and exploration',
       executionContext: '@agents/skills/claude-code/harness-brainstorming/SKILL.md',
       process: '1. Read SKILL.md and follow its workflow directly',
     },
   };

   const skillMdContent = '# Harness Brainstorming\n\nExplore ideas systematically.';

   describe('renderCodexSkill', () => {
     it('returns the SKILL.md content with generated header prepended', () => {
       const output = renderCodexSkill(skillMdContent);
       expect(output).toContain('# Harness Brainstorming');
       expect(output).toContain('Explore ideas systematically.');
       expect(output).toContain('Generated by harness generate-slash-commands');
     });
   });

   describe('renderCodexOpenaiYaml', () => {
     it('returns a YAML placeholder with name and version fields', () => {
       const output = renderCodexOpenaiYaml(baseSpec);
       expect(output).toContain('name: harness-brainstorming');
       expect(output).toContain('version: "1.0.0"');
     });

     it('includes the reserved-for-phase-b comment', () => {
       const output = renderCodexOpenaiYaml(baseSpec);
       expect(output).toContain('Reserved for Phase B');
     });
   });

   describe('renderCodexAgentsMd', () => {
     const specs = [
       baseSpec,
       {
         ...baseSpec,
         name: 'execution',
         skillYamlName: 'harness-execution',
         description: 'Execute plans',
       },
     ];

     it('includes a heading and intro paragraph', () => {
       const output = renderCodexAgentsMd(specs);
       expect(output).toContain('# Harness Skills');
     });

     it('lists all skill names', () => {
       const output = renderCodexAgentsMd(specs);
       expect(output).toContain('harness-brainstorming');
       expect(output).toContain('harness-execution');
     });

     it('includes skill descriptions', () => {
       const output = renderCodexAgentsMd(specs);
       expect(output).toContain('Structured ideation and exploration with harness methodology');
       expect(output).toContain('Execute plans');
     });

     it('includes the generated header', () => {
       const output = renderCodexAgentsMd(specs);
       expect(output).toContain('Generated by harness generate-slash-commands');
     });
   });
   ```

2. Run: `npx vitest run packages/cli/tests/slash-commands/render-codex.test.ts`
3. Observe failure: `Cannot find module '../../src/slash-commands/render-codex'`
4. Create `packages/cli/src/slash-commands/render-codex.ts`:

   ```typescript
   import type { SlashCommandSpec } from './types';
   import { GENERATED_HEADER_CODEX } from './types';

   export function renderCodexSkill(skillMdContent: string): string {
     return `<!-- ${GENERATED_HEADER_CODEX.replace(/<!--\s*|\s*-->/g, '').trim()} -->\n\n${skillMdContent}\n`;
   }

   export function renderCodexOpenaiYaml(spec: SlashCommandSpec): string {
     return [
       '# Reserved for Phase B native integration',
       `name: ${spec.skillYamlName}`,
       `version: "1.0.0"`,
       '',
     ].join('\n');
   }

   export function renderCodexAgentsMd(specs: SlashCommandSpec[]): string {
     const lines: string[] = [];
     lines.push(`<!-- ${GENERATED_HEADER_CODEX.replace(/<!--\s*|\s*-->/g, '').trim()} -->`);
     lines.push('');
     lines.push('# Harness Skills');
     lines.push('');
     lines.push(
       'This file bootstraps harness context for Codex CLI. Each skill is available as a structured workflow in the `harness/` directory.'
     );
     lines.push('');
     lines.push('## Available Skills');
     lines.push('');

     for (const spec of specs) {
       lines.push(`- **${spec.skillYamlName}** — ${spec.description}`);
     }

     lines.push('');
     return lines.join('\n');
   }
   ```

5. Run: `npx vitest run packages/cli/tests/slash-commands/render-codex.test.ts`
6. Observe: all tests pass
7. Run: `harness validate`
8. Commit: `feat(codex): implement render-codex.ts with SKILL.md, openai.yaml, and AGENTS.md renderers`

---

### Task 4: Wire new renderers into `generate-slash-commands.ts`

**Depends on:** Tasks 2, 3
**Files:** `packages/cli/src/commands/generate-slash-commands.ts`

1. Add imports at the top of `packages/cli/src/commands/generate-slash-commands.ts`:

   ```typescript
   import { renderCursor } from '../slash-commands/render-cursor';
   import {
     renderCodexSkill,
     renderCodexOpenaiYaml,
     renderCodexAgentsMd,
   } from '../slash-commands/render-codex';
   ```

2. Update `resolveOutputDir` to handle `codex` and `cursor`:

   ```typescript
   function resolveOutputDir(
     platform: Platform,
     opts: { global: boolean; output?: string }
   ): string {
     if (opts.output) {
       return path.join(opts.output, 'harness');
     }
     if (opts.global) {
       const home = os.homedir();
       if (platform === 'claude-code') return path.join(home, '.claude', 'commands', 'harness');
       if (platform === 'gemini-cli') return path.join(home, '.gemini', 'commands', 'harness');
       if (platform === 'cursor') return path.join(home, '.cursor', 'rules', 'harness');
       return path.join(home, '.codex', 'harness');
     }
     if (platform === 'claude-code')
       return path.join('agents', 'commands', 'claude-code', 'harness');
     if (platform === 'gemini-cli') return path.join('agents', 'commands', 'gemini-cli', 'harness');
     if (platform === 'cursor') return path.join('agents', 'commands', 'cursor', 'harness');
     return path.join('agents', 'commands', 'codex', 'harness');
   }
   ```

3. Update `fileExtension` to handle `codex` and `cursor`:

   ```typescript
   function fileExtension(platform: Platform): string {
     if (platform === 'claude-code') return '.md';
     if (platform === 'gemini-cli') return '.toml';
     if (platform === 'cursor') return '.mdc';
     return ''; // codex uses directories — handled separately
   }
   ```

4. In `generateSlashCommands`, replace the inner loop body. The current `if (platform === 'claude-code') ... else ...` block needs to become a 4-way dispatch. Replace lines 94–125 in the function (the `for (const spec of specs)` loop) with:

   ```typescript
   const rendered = new Map<string, string>();

   if (platform === 'codex') {
     // Codex uses a directory layout; collect per-skill files and write them
     // AGENTS.md is written to the parent dir (one level up from outputDir)
     const allSpecs: SlashCommandSpec[] = [];
     for (const spec of specs) {
       const mdPath = path.join(spec.skillsBaseDir, spec.sourceDir, 'SKILL.md');
       const skillMd = fs.existsSync(mdPath) ? fs.readFileSync(mdPath, 'utf-8') : '';
       const skillDir = path.join(outputDir, spec.skillYamlName);
       const agentsSubDir = path.join(skillDir, 'agents');
       if (!opts.dryRun) {
         fs.mkdirSync(agentsSubDir, { recursive: true });
         fs.writeFileSync(path.join(skillDir, 'SKILL.md'), renderCodexSkill(skillMd), 'utf-8');
         fs.writeFileSync(
           path.join(agentsSubDir, 'openai.yaml'),
           renderCodexOpenaiYaml(spec),
           'utf-8'
         );
       }
       allSpecs.push(spec);
     }
     // Write top-level AGENTS.md one directory above outputDir
     const codexRoot = path.dirname(outputDir);
     if (!opts.dryRun) {
       fs.mkdirSync(codexRoot, { recursive: true });
       fs.writeFileSync(path.join(codexRoot, 'AGENTS.md'), renderCodexAgentsMd(allSpecs), 'utf-8');
     }
     // Return early — codex doesn't use the sync plan mechanism
     results.push({
       platform,
       added: specs.map((s) => s.skillYamlName),
       updated: [],
       removed: [],
       unchanged: [],
       outputDir,
     });
     continue;
   }

   for (const spec of specs) {
     if (platform === 'cursor') {
       const filename = `${spec.skillYamlName}.mdc`;
       const mdPath = path.join(spec.skillsBaseDir, spec.sourceDir, 'SKILL.md');
       const yamlPath = path.join(spec.skillsBaseDir, spec.sourceDir, 'skill.yaml');
       const skillMd = fs.existsSync(mdPath) ? fs.readFileSync(mdPath, 'utf-8') : '';
       const cursorConfig = (() => {
         if (!fs.existsSync(yamlPath)) return undefined;
         try {
           const { parse: parseYaml } = await import('yaml');
           // Use synchronous parse since we already have the content
           const raw = fs.readFileSync(yamlPath, 'utf-8');
           const meta = parseYaml(raw) as { cursor?: { globs?: string[]; alwaysApply?: boolean } };
           return meta.cursor;
         } catch {
           return undefined;
         }
       })();
       rendered.set(filename, renderCursor(spec, skillMd, cursorConfig));
     } else if (platform === 'claude-code') {
       const filename = `${spec.name}.md`;
       const useAbsolutePaths = opts.global;
       const renderSpec = useAbsolutePaths
         ? {
             ...spec,
             prompt: {
               ...spec.prompt,
               executionContext: spec.prompt.executionContext
                 .split('\n')
                 .map((line) => {
                   if (line.startsWith('@')) {
                     const relPath = line.slice(1);
                     return `@${path.resolve(relPath)}`;
                   }
                   return line;
                 })
                 .join('\n'),
             },
           }
         : spec;
       rendered.set(filename, renderClaudeCode(renderSpec));
     } else {
       // gemini-cli
       const filename = `${spec.name}.toml`;
       const mdPath = path.join(spec.skillsBaseDir, spec.sourceDir, 'SKILL.md');
       const yamlPath = path.join(spec.skillsBaseDir, spec.sourceDir, 'skill.yaml');
       const mdContent = fs.existsSync(mdPath) ? fs.readFileSync(mdPath, 'utf-8') : '';
       const yamlContent = fs.existsSync(yamlPath) ? fs.readFileSync(yamlPath, 'utf-8') : '';
       rendered.set(filename, renderGemini(spec, mdContent, yamlContent));
     }
   }
   ```

   **Note on cursor YAML reading:** The `await import('yaml')` pattern won't work in a synchronous function. Instead, add `import { parse as parseYaml } from 'yaml';` to the top-level imports and use it directly. Replace the IIFE approach with:

   ```typescript
   const cursorConfig = (() => {
     if (!fs.existsSync(yamlPath)) return undefined;
     try {
       const raw = fs.readFileSync(yamlPath, 'utf-8');
       const meta = parseYaml(raw) as { cursor?: { globs?: string[]; alwaysApply?: boolean } };
       return meta.cursor;
     } catch {
       return undefined;
     }
   })();
   ```

   Add `import { parse as parseYaml } from 'yaml';` to the imports block.

5. Update the command description string from `'Generate native slash commands for Claude Code and Gemini CLI from skill metadata'` to `'Generate native commands for Claude Code, Gemini CLI, Codex CLI, and Cursor from skill metadata'`.

6. Run: `npx vitest run packages/cli/tests/slash-commands/integration.test.ts`
7. Run: `npx vitest run packages/cli/tests/commands/generate.test.ts`
8. Observe: existing tests pass
9. Run: `harness validate`
10. Commit: `feat(generator): wire codex and cursor into generate-slash-commands dispatch`

---

### Task 5: Add skill schema tests for new optional blocks

**Depends on:** Task 1
**Files:** `packages/cli/tests/skill/schema.test.ts`

1. Open `packages/cli/tests/skill/schema.test.ts`. Append new test cases to the existing `describe('SkillMetadataSchema')` block:

   ```typescript
   it('accepts cursor block with alwaysApply and globs', () => {
     const result = SkillMetadataSchema.parse({
       ...validBase,
       cursor: { globs: ['src/**/*.ts'], alwaysApply: true },
     });
     expect(result.cursor?.alwaysApply).toBe(true);
     expect(result.cursor?.globs).toEqual(['src/**/*.ts']);
   });

   it('accepts cursor block with defaults', () => {
     const result = SkillMetadataSchema.parse({
       ...validBase,
       cursor: {},
     });
     expect(result.cursor?.alwaysApply).toBe(false);
   });

   it('accepts codex block with instructions_override', () => {
     const result = SkillMetadataSchema.parse({
       ...validBase,
       codex: { instructions_override: 'Custom instructions here' },
     });
     expect(result.codex?.instructions_override).toBe('Custom instructions here');
   });

   it('accepts codex block without instructions_override', () => {
     const result = SkillMetadataSchema.parse({
       ...validBase,
       codex: {},
     });
     expect(result.codex).toBeDefined();
     expect(result.codex?.instructions_override).toBeUndefined();
   });

   it('accepts skill with codex platform in platforms array', () => {
     const result = SkillMetadataSchema.parse({
       ...validBase,
       platforms: ['claude-code', 'codex'],
     });
     expect(result.platforms).toContain('codex');
   });

   it('accepts skill with cursor platform in platforms array', () => {
     const result = SkillMetadataSchema.parse({
       ...validBase,
       platforms: ['claude-code', 'cursor'],
     });
     expect(result.platforms).toContain('cursor');
   });
   ```

2. Run: `npx vitest run packages/cli/tests/skill/schema.test.ts`
3. Observe: all tests pass (including new ones)
4. Run: `harness validate`
5. Commit: `test(schema): add coverage for cursor and codex optional blocks in skill.yaml`

---

### Task 6: Create skill directory symlinks for codex

**Depends on:** Task 1
**Files:** `agents/skills/codex/` (80 symlinks)

1. Run the following script to create the codex skill directory and all symlinks:

   ```bash
   mkdir -p /Users/cwarner/Projects/harness-engineering/agents/skills/codex
   cd /Users/cwarner/Projects/harness-engineering/agents/skills/codex
   for skill in $(ls ../claude-code/); do
     ln -sfn "../claude-code/${skill}" "${skill}"
   done
   ```

2. Verify count:

   ```bash
   ls /Users/cwarner/Projects/harness-engineering/agents/skills/codex/ | wc -l
   ```

   Observe: `80`

3. Verify a sample symlink resolves correctly:

   ```bash
   readlink /Users/cwarner/Projects/harness-engineering/agents/skills/codex/harness-brainstorming
   ```

   Observe: `../claude-code/harness-brainstorming`

4. Run: `harness validate`
5. Commit: `feat(codex): create agents/skills/codex/ with 80 symlinks to claude-code/`

---

### Task 7: Create skill directory symlinks for cursor

**Depends on:** Task 1
**Files:** `agents/skills/cursor/` (80 symlinks)

1. Run the following script to create the cursor skill directory and all symlinks:

   ```bash
   mkdir -p /Users/cwarner/Projects/harness-engineering/agents/skills/cursor
   cd /Users/cwarner/Projects/harness-engineering/agents/skills/cursor
   for skill in $(ls ../claude-code/); do
     ln -sfn "../claude-code/${skill}" "${skill}"
   done
   ```

2. Verify count:

   ```bash
   ls /Users/cwarner/Projects/harness-engineering/agents/skills/cursor/ | wc -l
   ```

   Observe: `80`

3. Verify a sample symlink resolves correctly:

   ```bash
   readlink /Users/cwarner/Projects/harness-engineering/agents/skills/cursor/harness-brainstorming
   ```

   Observe: `../claude-code/harness-brainstorming`

4. Run: `harness validate`
5. Commit: `feat(cursor): create agents/skills/cursor/ with 80 symlinks to claude-code/`

---

### Task 8: Update and run platform parity tests

**Depends on:** Tasks 6, 7
**Files:** `agents/skills/tests/platform-parity.test.ts`, `agents/skills/tests/schema.ts`

The platform parity test dynamically discovers platforms using `ALLOWED_PLATFORMS` from `schema.ts`. Since Task 1 already updates `agents/skills/tests/schema.ts` to include `codex` and `cursor`, and Tasks 6/7 create the symlinks, the parity tests should now pick up all four platforms automatically.

1. Run the parity test suite:

   ```bash
   npx vitest run agents/skills/tests/platform-parity.test.ts
   ```

2. Observe expected behavior: all four platforms (`claude-code`, `gemini-cli`, `codex`, `cursor`) have 80 skills each. Note: `gemini-cli` currently has 79 symlinks (missing `harness-supply-chain-audit`). The parity test will detect this.

   **If the test fails due to gemini-cli missing `harness-supply-chain-audit`:** This is a pre-existing gap. Fix it by creating the missing symlink:

   ```bash
   cd /Users/cwarner/Projects/harness-engineering/agents/skills/gemini-cli
   ln -sfn "../claude-code/harness-supply-chain-audit" "harness-supply-chain-audit"
   ```

   Then re-run the test.

3. Observe: all parity tests pass — skill counts match across all platforms, and skill files are identical (symlinks read through to the same source).

4. Run: `harness validate`
5. Commit: `fix(parity): ensure all platforms have 80 skills; fix gemini-cli missing harness-supply-chain-audit symlink`

   (If no fix was needed, use: `test(parity): verify platform parity for codex and cursor`)

---

### Task 9: End-to-end generation smoke test and final validation

[checkpoint:human-verify]

**Depends on:** Tasks 4, 6, 7, 8
**Files:** `agents/commands/codex/`, `agents/commands/cursor/`

1. Run cursor generation:

   ```bash
   cd /Users/cwarner/Projects/harness-engineering
   node packages/cli/dist/index.js generate-slash-commands --platform cursor --skills-dir agents/skills/claude-code --yes 2>&1 || npx tsx packages/cli/src/index.ts generate-slash-commands --platform cursor --skills-dir agents/skills/claude-code --yes
   ```

   Alternatively use: `harness generate-slash-commands --platform cursor --yes`

2. Verify cursor output:

   ```bash
   ls agents/commands/cursor/harness/ | wc -l
   head -10 agents/commands/cursor/harness/harness-brainstorming.mdc
   ```

   Observe: 80 `.mdc` files; first file starts with `---` and contains `description:`, `alwaysApply:` frontmatter.

3. Run codex generation:

   ```bash
   harness generate-slash-commands --platform codex --yes
   ```

4. Verify codex output:

   ```bash
   ls agents/commands/codex/harness/ | wc -l
   cat agents/commands/codex/AGENTS.md | head -20
   cat agents/commands/codex/harness/harness-brainstorming/SKILL.md | head -5
   cat agents/commands/codex/harness/harness-brainstorming/agents/openai.yaml
   ```

   Observe: 80 skill directories; AGENTS.md lists all skills; SKILL.md has content; openai.yaml has placeholder with name and version.

5. Verify existing platforms unaffected:

   ```bash
   harness generate-slash-commands --platform claude-code --dry-run
   harness generate-slash-commands --platform gemini-cli --dry-run
   ```

   Observe: dry-run shows `= N unchanged` (no unexpected updates).

6. Run full test suite:

   ```bash
   npx vitest run packages/cli/tests/slash-commands/
   npx vitest run agents/skills/tests/
   ```

   Observe: all tests pass.

7. Run: `harness validate`
8. Commit: `feat(platforms): complete Phase A — codex and cursor platform generation working end-to-end`

---

## Dependency Order Summary

```
Task 1 (schema) ──────────────────────────────────────────┐
                 ├── Task 2 (render-cursor) ──────────────┤
                 ├── Task 3 (render-codex) ───────────────┤── Task 4 (generator wiring)
                 ├── Task 5 (schema tests)                │
                 ├── Task 6 (codex symlinks) ─────────────┤
                 └── Task 7 (cursor symlinks) ────────────┤
                                                          └── Task 8 (parity tests) ── Task 9 (smoke test)
```

Tasks 2, 3, 5, 6, 7 are all independent of each other and can proceed in parallel after Task 1.

---

## Estimated Time

| Task                          | Estimate    |
| ----------------------------- | ----------- |
| Task 1: Schema updates        | 5 min       |
| Task 2: render-cursor (TDD)   | 5 min       |
| Task 3: render-codex (TDD)    | 5 min       |
| Task 4: Generator wiring      | 8 min       |
| Task 5: Schema tests          | 3 min       |
| Task 6: Codex symlinks        | 2 min       |
| Task 7: Cursor symlinks       | 2 min       |
| Task 8: Parity tests          | 5 min       |
| Task 9: Smoke test + validate | 5 min       |
| **Total**                     | **~40 min** |

---

## Observable Truth Traceability

| Observable Truth                                    | Delivered By             |
| --------------------------------------------------- | ------------------------ |
| cursor generates 80 `.mdc` files                    | Tasks 2, 4, 7, 9         |
| codex generates 80 `SKILL.md` + `openai.yaml`       | Tasks 3, 4, 6, 9         |
| codex generates `AGENTS.md`                         | Tasks 3, 4, 9            |
| skill.yaml accepts `codex:` and `cursor:` blocks    | Tasks 1, 5               |
| `alwaysApply: true` reflected in `.mdc` frontmatter | Task 2                   |
| `globs` reflected in `.mdc` frontmatter             | Task 2                   |
| `agents/skills/codex/` has 80 symlinks              | Task 6                   |
| `agents/skills/cursor/` has 80 symlinks             | Task 7                   |
| Platform parity tests pass                          | Task 8                   |
| `harness validate` passes                           | Tasks 1–9 (each runs it) |
| Existing platforms unaffected                       | Task 9 verification      |
