From 3ed4a4fa0f6f4d08144ae7598af391b4f070b649 Mon Sep 17 00:00:00 2001 From: Trevin Chow Date: Mon, 20 Apr 2026 19:44:25 -0700 Subject: [PATCH] feat(codex): native plugin install manifests + agents-only converter (#616) --- .agents/plugins/marketplace.json | 32 + .github/release-please-config.json | 10 + README.md | 47 +- ...feat-codex-native-plugin-manifests-plan.md | 695 ++++++++++++++++++ .../coding-tutor/.codex-plugin/plugin.json | 33 + .../.codex-plugin/plugin.json | 45 ++ plugins/compound-engineering/AGENTS.md | 23 + scripts/release/validate.ts | 10 +- src/commands/cleanup.ts | 22 + src/commands/convert.ts | 7 + src/commands/install.ts | 7 + src/converters/claude-to-codex.ts | 56 +- src/converters/claude-to-opencode.ts | 18 + src/release/metadata.ts | 129 +++- src/targets/codex.ts | 24 +- src/types/codex.ts | 9 + src/utils/legacy-cleanup.ts | 41 ++ tests/cli.test.ts | 53 +- tests/codex-converter.test.ts | 62 ++ tests/codex-writer.test.ts | 100 +++ tests/release-metadata.test.ts | 240 ++++++ 21 files changed, 1649 insertions(+), 14 deletions(-) create mode 100644 .agents/plugins/marketplace.json create mode 100644 docs/plans/2026-04-20-001-feat-codex-native-plugin-manifests-plan.md create mode 100644 plugins/coding-tutor/.codex-plugin/plugin.json create mode 100644 plugins/compound-engineering/.codex-plugin/plugin.json diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..d79815c --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,32 @@ +{ + "name": "compound-engineering-plugin", + "interface": { + "displayName": "Compound Engineering" + }, + "plugins": [ + { + "name": "compound-engineering", + "source": { + "source": "local", + "path": "./plugins/compound-engineering" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Coding" + }, + { + "name": "coding-tutor", + "source": { + "source": "local", + "path": "./plugins/coding-tutor" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Coding" + } + ] +} diff --git a/.github/release-please-config.json b/.github/release-please-config.json index 5e55e4c..40a37fd 100644 --- a/.github/release-please-config.json +++ b/.github/release-please-config.json @@ -56,6 +56,11 @@ "type": "json", "path": ".cursor-plugin/plugin.json", "jsonpath": "$.version" + }, + { + "type": "json", + "path": ".codex-plugin/plugin.json", + "jsonpath": "$.version" } ] }, @@ -72,6 +77,11 @@ "type": "json", "path": ".cursor-plugin/plugin.json", "jsonpath": "$.version" + }, + { + "type": "json", + "path": ".codex-plugin/plugin.json", + "jsonpath": "$.version" } ] }, diff --git a/README.md b/README.md index 6e3a028..52fbbf3 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,47 @@ In Cursor Agent chat, install from the plugin marketplace: Or search for "compound engineering" in the plugin marketplace. +### Codex + +Three steps: register the marketplace, install the agent set, then enable the plugin inside Codex. + +1. **Register the marketplace with Codex:** + + ```bash + codex plugin marketplace add EveryInc/compound-engineering-plugin + ``` + +2. **Install the agent set** (Codex's plugin spec doesn't register custom agents yet): + + ```bash + bunx @every-env/compound-plugin install compound-engineering --to codex + ``` + +3. **Enable the plugin inside Codex:** launch `codex`, run `/plugins`, find the **Compound Engineering** marketplace, select the **compound-engineering** plugin, and choose **Install**. Restart Codex after install completes. Codex's CLI doesn't currently have a subcommand for enabling a plugin from an added marketplace — the `/plugins` TUI is the canonical flow. + +All three steps are needed. The marketplace registration + TUI install handles skills; the Bun step adds the review, research, and workflow agents that skills like `ce-code-review`, `ce-plan`, and `ce-work` spawn via `Task`. Without the agent step, delegating skills will report missing agents. The Bun step defaults to agents-only so it doesn't double-register skills. + +> **Heads up:** once Codex's native plugin spec supports custom agents, the Bun agent step goes away — the TUI install alone will be sufficient. + +If you previously used the Bun-only Codex install, back up stale CE artifacts before switching (safe to re-run): + +```bash +bunx @every-env/compound-plugin cleanup --target codex +``` + +
+Standalone install without codex plugin marketplace add + +If you can't use Codex's plugin marketplace for some reason, the Bun converter can emit the full bundle on its own: + +```bash +bunx @every-env/compound-plugin install compound-engineering --to codex --include-skills +``` + +Don't combine this with the marketplace + `/plugins` install — skills will register twice. The recommended path is the three-step flow above. + +
+ ### GitHub Copilot CLI Inside Copilot CLI: @@ -126,10 +167,10 @@ If you previously used the old Bun Qwen install, back up stale CE artifacts befo bunx @every-env/compound-plugin cleanup --target qwen ``` -### OpenCode, Codex, Pi, Gemini & Kiro (experimental) +### OpenCode, Pi, Gemini & Kiro (experimental) -This repo includes a Bun/TypeScript CLI that converts Claude Code plugins to OpenCode, Codex, Pi, Gemini CLI, and Kiro CLI. -Use the native plugin install instructions above for Claude Code, Cursor, GitHub Copilot CLI, Factory Droid, and Qwen Code. +This repo includes a Bun/TypeScript CLI that converts Claude Code plugins to OpenCode, Pi, Gemini CLI, and Kiro CLI. +Use the native plugin install instructions above for Claude Code, Cursor, Codex, GitHub Copilot CLI, Factory Droid, and Qwen Code. ```bash # convert the compound-engineering plugin into OpenCode format diff --git a/docs/plans/2026-04-20-001-feat-codex-native-plugin-manifests-plan.md b/docs/plans/2026-04-20-001-feat-codex-native-plugin-manifests-plan.md new file mode 100644 index 0000000..e8f3b48 --- /dev/null +++ b/docs/plans/2026-04-20-001-feat-codex-native-plugin-manifests-plan.md @@ -0,0 +1,695 @@ +--- +title: "feat: Ship Codex-format plugin manifests alongside Claude manifests" +type: feat +status: active +date: 2026-04-20 +--- + +# feat: Ship Codex-format plugin manifests alongside Claude manifests + +## Overview + +Add Codex-format plugin manifests (`.agents/plugins/marketplace.json` plus per-plugin `.codex-plugin/plugin.json`) to the repo alongside the existing Claude-format manifests, so Codex users can install CE's skills via the native `codex plugin marketplace add EveryInc/compound-engineering-plugin` flow. + +Agents are not supported by Codex's native plugin spec, so the existing Bun converter (`bunx @every-env/compound-plugin install compound-engineering --to codex`) remains required to complete a CE install. To prevent skill double-registration when users run both flows, the Bun converter's `--to codex` default is changed to **agents-only**; an opt-in `--include-skills` flag re-enables the full bundle for standalone installs. The README documents the two-step flow. + +## Problem Frame + +Codex is the only target in CE's installable set still gated on the Bun converter for the baseline (skills) install. Every other tool either has native support (Claude Code, Cursor, Copilot, Droid, Qwen) or has no native install mechanism at all (OpenCode, Pi, Gemini, Kiro). Codex does have a native plugin format — we just never shipped the manifests for it. + +Shipping the Codex manifests: + +* Puts Codex in the "native install" tier alongside Copilot/Droid/Qwen for discovery and lifecycle (install/uninstall/update via `codex plugin`) + +* Does not change the agent install path (native Codex plugin install does not register custom agents per the spec and our empirical test) + +* Costs \~two hand-authored JSON files per plugin plus a small release-infra extension, because the repo already supports dual-format manifests (Claude + Cursor) and adding a third format is a parallel entry, not a new pattern + +## Requirements Trace + +* R1. `codex plugin marketplace add ` must succeed and register the CE plugin + +* R2. `codex plugin install compound-engineering` must install CE's skills into the expected Codex skill location + +* R3. Plugin version in `.codex-plugin/plugin.json` must stay in sync with `.claude-plugin/plugin.json` automatically on release + +* R4. `bun run release:validate` must fail if the Codex manifests drift out of sync with the Claude manifests (plugin list mismatch, name mismatch, version mismatch) + +* R5. README documents the Codex native install flow with a followup step for agents + +* R6. No regressions to existing Claude, Cursor, Copilot, Droid, Qwen, or Bun-converter install paths + +## Scope Boundaries + +* Native Codex plugin install handles skills only (Codex spec does not register custom agents or slash commands). Agents still flow through the Bun converter; the converter's default behavior is changed in Unit 9 so skills are NOT emitted by default, preventing double-registration. + +* Commands are not installed via native Codex plugin install (Codex spec limitation). Only affects the `coding-tutor` plugin, which ships commands. Coding-tutor users wanting commands run the Bun converter with `--include-skills`. + +* No single-command hybrid UX (the two-step `codex plugin install` + `bunx ... --to codex` flow is documented, not automated). This becomes obsolete when Codex supports custom agents natively — at which point the entire `--to codex` converter path is deprecated. + +* No logo asset — `interface.logo` is omitted; can be added in a followup when a branded icon is available + +* No Codex-specific skill frontmatter fields (`metadata.priority`, `metadata.pathPatterns`, `metadata.bashPatterns`) — these are trigger-tuning extensions, not required for registration, and can be added per-skill in followups + +* No empirical test of remote-repo install in this plan. The remote `codex plugin marketplace add EveryInc/compound-engineering-plugin` flow documented in the README cannot be tested from a feature branch — Codex fetches the default branch of the remote. Remote-install verification is a separate manual step immediately post-merge, before the release tag: clone the merged `main`, run the remote install command against it, confirm skills register. If the remote path fails, ship a fix-forward PR rather than rolling back. `source: { source: "local", path: "./plugins/" }` has been empirically verified as the correct schema for both bundled AND remote-cloned marketplaces (see Resolved Open Questions), so the most likely remote-vs-local divergence — the schema — is already de-risked + +### Deferred to Separate Tasks + +* Hybrid install UX that bundles `codex plugin install` with the agent followup into a single command: future plan once Codex's native spec is more settled + +* Codex-specific skill metadata tuning (priority, path patterns, bash patterns) for discoverability: evaluate per-skill in followups as use patterns emerge + +* Plugin logo asset design: hand off to design; drop in later + +* Removal of the `--to codex` Bun converter path entirely once Codex supports custom agents natively; at that point `codex plugin install` is sufficient on its own + +## Context & Research + +### Relevant Code and Patterns + +* `.claude-plugin/marketplace.json`, `.cursor-plugin/marketplace.json` — existing dual-format marketplace manifests (Cursor mirrors Claude's schema; Codex will diverge) + +* `plugins/compound-engineering/.claude-plugin/plugin.json`, `.cursor-plugin/plugin.json` — existing dual plugin manifests (source of truth for name/description/version/author/homepage/keywords) + +* `.github/release-please-config.json` — `plugins/compound-engineering` and `plugins/coding-tutor` packages already list `extra-files` for `.claude-plugin/plugin.json` and `.cursor-plugin/plugin.json`; Codex adds a third entry in each + +* `.github/.release-please-manifest.json` — tracks versions per release-please package; Cursor marketplace (`.cursor-plugin`) is a separate tracked package, Codex likely does not need its own tracked package since the Codex marketplace spec has no `version` field (see Key Technical Decisions) + +* `src/release/components.ts` — declares release components (`marketplace`, `cursor-marketplace`, CLI, per-plugin) and their source-of-truth file paths + +* `src/release/metadata.ts` — sync engine that reads the various marketplace + plugin manifests and cross-checks / updates versions and descriptions + +* `src/release/config.ts` — validator stubs (currently only checks `changelog-path` shape); extend here or in `metadata.ts` for Codex-consistency rules + +* `scripts/release/validate.ts` — entry point run by `bun run release:validate`; consumes the above + +* `tests/release-components.test.ts`, `tests/release-config.test.ts`, `tests/release-metadata.test.ts` — existing test coverage for the release infra; extend alongside the code changes + +### External References + +* Codex plugin docs: [developers.openai.com/codex/plugins](https://developers.openai.com/codex/plugins), [developers.openai.com/codex/plugins/build](https://developers.openai.com/codex/plugins/build) + +* Canonical reference repo: `github.com/openai/plugins` — confirms `.agents/plugins/marketplace.json` at repo root, `.codex-plugin/plugin.json` per plugin + +* Local evidence: + + * `~/.codex/.tmp/bundled-marketplaces/openai-bundled/.agents/plugins/marketplace.json` — bundled OpenAI example, minimal shape + + * `~/.codex/.tmp/plugins/plugins/vercel/` — fully-featured plugin with skills; shows `"skills": "./skills/"` declaration pattern and `interface{}` block shape + +### Documented Codex format (worked out from sources above) + +**`.agents/plugins/marketplace.json`** (repo root; Codex looks here after cloning): + +```json +{ + "name": "compound-engineering-plugin", + "interface": { "displayName": "Compound Engineering" }, + "plugins": [ + { + "name": "compound-engineering", + "source": { "source": "local", "path": "./plugins/compound-engineering" }, + "policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" }, + "category": "Coding" + } + ] +} +``` + +**`plugins//.codex-plugin/plugin.json`**: + +```json +{ + "name": "...", + "version": "...", + "description": "...", + "author": { "name": "...", "email": "...", "url": "..." }, + "homepage": "...", + "repository": "...", + "license": "...", + "keywords": ["..."], + "skills": "./skills/", + "interface": { + "displayName": "...", + "shortDescription": "...", + "longDescription": "...", + "developerName": "...", + "category": "Coding", + "capabilities": ["Interactive", "Read", "Write"], + "websiteURL": "...", + "privacyPolicyURL": "...", + "termsOfServiceURL": "...", + "defaultPrompt": ["..."], + "screenshots": [] + } +} +``` + +Required fields per docs: `name`, `version`, `description`. All others optional. Native install registers skills (via `skills:` key), MCP servers (`mcpServers:`), apps (`apps:`), hooks (`hooks:`). Agents, commands, and prompts are not declarable or auto-discovered. + +## Key Technical Decisions + +* **Commit manifests, don't generate.** Hand-authored, versioned like source. release-please bumps `version` in `.codex-plugin/plugin.json` via `extra-files`, same mechanism already used for Claude + Cursor. + +* **Don't track the Codex marketplace as a release-please package.** The Codex marketplace spec (`.agents/plugins/marketplace.json`) has no `version` field — unlike the Claude and Cursor marketplaces which have `metadata.version`. Treat the Codex marketplace as static content; only the per-plugin `.codex-plugin/plugin.json` version needs automated bumping. + +* **Extend** **`src/release/metadata.ts`** **to read the Codex manifests and cross-check them.** Mirrors how Cursor manifests were added: read them, cross-reference plugin lists and versions against the Claude source of truth, fail validation on drift. + +* **Omit** **`interface.logo`** **for now.** Optional per docs; the bundled OpenAI example has one but many listed plugins don't. Ship without, add later when an icon is available. + +* **Don't add Codex-specific skill frontmatter extensions.** `metadata.priority`, `metadata.pathPatterns`, `metadata.bashPatterns` are trigger-tuning optimizations, not required for registration. CE skills will use their current Claude-compatible frontmatter; Codex will register them with default trigger behavior. + +* **`coding-tutor`** **still needs a Codex manifest** even though native install won't handle its commands. Reason: the marketplace lists both plugins as a unit; omitting coding-tutor from the Codex marketplace would be asymmetric with the Claude marketplace. Native install will successfully install coding-tutor's skills but not its commands — the README's coding-tutor install instructions will note that commands require the Bun converter. + +* **Validation failure modes to enforce:** missing Codex manifest when Claude manifest exists; plugin list mismatch between `.claude-plugin/marketplace.json` and `.agents/plugins/marketplace.json`; name mismatch between paired plugin.json files; version mismatch between paired plugin.json files; declared `skills: "./skills/"` pointing at a missing directory. + +## Open Questions + +### Resolved During Planning + +* **Do we need to ship a logo?** No — omit the field. Add in a followup when an asset is available. + +* **Should skills declare Codex metadata extensions?** No — ship with default trigger behavior. Add per-skill tuning in followups if use patterns reveal a need. + +* **Is the Codex marketplace a release-please package?** No — it has no version field per the Codex spec, so it stays static. Per-plugin `.codex-plugin/plugin.json` is the only versioned file. + +* **Does** **`coding-tutor`** **get a Codex manifest?** Yes — marketplace parity with Claude. Native install will register its skills but not its commands; README notes the gap. + +* **Are file paths for the** **`skills:`** **declaration plugin-relative or marketplace-relative?** Plugin-relative. `"skills": "./skills/"` in `plugins/compound-engineering/.codex-plugin/plugin.json` means `plugins/compound-engineering/skills/`. Confirmed via vercel and github plugin examples. + +* **Does the `source: "local"` marketplace schema work for remote-cloned marketplaces, not just bundled ones?** Yes. The `openai-curated` marketplace (a real-world remote-fetched marketplace Codex clones and caches at `~/.codex/.tmp/plugins/.agents/plugins/marketplace.json`) uses the identical `source: { source: "local", path: "./plugins/" }` schema. "local" refers to the plugin's co-location within the marketplace repo, not "bundled with Codex." Same schema for both. + +* **Does Codex's default skill discovery find flat `skills//SKILL.md` layouts at CE's depth?** Yes. Vercel's reference plugin at `~/.codex/.tmp/plugins/plugins/vercel/skills/` uses the exact layout CE ships — flat subdirectories each containing `SKILL.md`. CE has 43 skill directories at that depth under `plugins/compound-engineering/skills/`. Unit 7 includes a count-based assertion to catch partial-discovery regressions. + +### Deferred to Implementation + +* **Exact** **`interface.shortDescription`** **/** **`longDescription`** **copy for each plugin.** Use the `description` from `.claude-plugin/plugin.json` as the short form; compose a longer version from the plugin's README section or existing marketplace description. Can be refined during implementation. + +* **Does** **`codex plugin install`** **succeed against a local clone of this branch?** Empirical verification happens during implementation. If the plugin manifest schema is rejected (e.g., a required field we didn't identify from docs), iterate. + +* **Does the Codex skills mechanism register CE's skills without modification?** Local empirical test during implementation. CE skills use standard Claude frontmatter (`name`, `description`); Codex docs say those are the required fields. Expected to work. + +## Implementation Units + +* [ ] **Unit 1: Author** **`plugins/compound-engineering/.codex-plugin/plugin.json`** + +**Goal:** Codex plugin manifest for the primary CE plugin, with skills declared and interface metadata populated. + +**Requirements:** R1, R2 + +**Dependencies:** None + +**Files:** + +* Create: `plugins/compound-engineering/.codex-plugin/plugin.json` + +**Approach:** + +* Read the Claude manifest at `plugins/compound-engineering/.claude-plugin/plugin.json` for source-of-truth fields (name, version, description, author, homepage, license, keywords). + +* Add Codex-specific fields: `skills: "./skills/"`, and an `interface{}` block with `displayName`, `shortDescription` (reuse `description`), `longDescription` (1-2 sentence pitch, can draw from README lead paragraph), `developerName` (derive from author), `category: "Coding"`, `capabilities: ["Interactive", "Read", "Write"]`, `websiteURL: homepage`, `privacyPolicyURL` / `termsOfServiceURL` (reuse Every's existing policy URLs if available; omit otherwise — optional per docs), `defaultPrompt: []` (can leave empty or add 2-3 starter prompts). + +* Omit `logo` (decided in Key Technical Decisions). + +* Omit `mcpServers`, `apps`, `hooks` (CE doesn't ship these). + +**Patterns to follow:** + +* `plugins/compound-engineering/.claude-plugin/plugin.json` — source of truth for shared fields + +* `~/.codex/.tmp/plugins/plugins/vercel/.codex-plugin/plugin.json` (locally cached) — real-world reference for `interface{}` field shape and `skills:` declaration + +* `~/.codex/.tmp/plugins/plugins/github/.codex-plugin/plugin.json` (locally cached) — another skills-declaring reference + +**Test scenarios:** + +* Test expectation: none -- pure content addition, no code. Functional verification happens in Unit 7 (empirical install test). + +**Verification:** + +* File exists and parses as valid JSON + +* `jq` queries return expected values: `.name == "compound-engineering"`, `.skills == "./skills/"`, `.interface.displayName` non-empty + +*** + +* [ ] **Unit 2: Author** **`plugins/coding-tutor/.codex-plugin/plugin.json`** + +**Goal:** Codex plugin manifest for the secondary CE plugin. + +**Requirements:** R1 + +**Dependencies:** None (parallel to Unit 1) + +**Files:** + +* Create: `plugins/coding-tutor/.codex-plugin/plugin.json` + +**Approach:** + +* Same approach as Unit 1, using `plugins/coding-tutor/.claude-plugin/plugin.json` as source of truth. + +* `coding-tutor` ships skills + commands. Declare only `skills: "./skills/"` — commands are not installable via native Codex plugin install (Codex spec limitation). + +* Keep `interface.longDescription` honest about what's available via native install (skills only); users who want commands are directed to the Bun converter via README. + +**Patterns to follow:** + +* Unit 1 (mirror the structure and field choices) + +* `plugins/coding-tutor/.claude-plugin/plugin.json` + +**Test scenarios:** + +* Test expectation: none -- pure content addition. + +**Verification:** + +* File exists, valid JSON, `jq` queries return expected values + +*** + +* [ ] **Unit 3: Author** **`.agents/plugins/marketplace.json`** + +**Goal:** Codex marketplace manifest at the repo root, listing both CE plugins, so `codex plugin marketplace add ` succeeds. + +**Requirements:** R1 + +**Dependencies:** Unit 1, Unit 2 (the marketplace references both plugin manifests) + +**Files:** + +* Create: `.agents/plugins/marketplace.json` + +**Approach:** + +* Schema per the Codex docs and bundled OpenAI example: + + * `name: "compound-engineering-plugin"` (matches Claude marketplace's `name`) + + * `interface.displayName: "Compound Engineering"` + + * `plugins[]` with two entries, one per plugin, each using the nested `source: { source: "local", path: "./plugins/" }` shape + + * Each plugin entry: `policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" }`, `category: "Coding"` + +* No `version` field (Codex spec doesn't require one; keeps this file static). + +* No `owner` field (Codex marketplace schema doesn't include it — owner info lives in each plugin's `.codex-plugin/plugin.json` via `author`). + +**Patterns to follow:** + +* `~/.codex/.tmp/bundled-marketplaces/openai-bundled/.agents/plugins/marketplace.json` — canonical schema reference + +* `.claude-plugin/marketplace.json` — for deciding which plugins to list (maintain parity) + +**Test scenarios:** + +* Test expectation: none -- pure content addition. + +**Verification:** + +* File exists, valid JSON + +* `.plugins | length == 2` + +* Plugin names match those in `.claude-plugin/marketplace.json` + +*** + +* [ ] **Unit 4: Extend release-please config to bump** **`.codex-plugin/plugin.json`** **versions** + +**Goal:** On each release, release-please updates `version` in both `.codex-plugin/plugin.json` files alongside the existing `.claude-plugin/plugin.json` and `.cursor-plugin/plugin.json` bumps. + +**Requirements:** R3 + +**Dependencies:** Units 1 and 2 (the files must exist for release-please to update them) + +**Files:** + +* Modify: `.github/release-please-config.json` + +**Approach:** + +* For the `plugins/compound-engineering` package entry, add a third entry to `extra-files`: + + ``` + { "type": "json", "path": ".codex-plugin/plugin.json", "jsonpath": "$.version" } + ``` + +* Same addition to the `plugins/coding-tutor` package entry. + +* No new top-level package for Codex marketplace — `.agents/plugins/marketplace.json` is static (no version field). + +* No changes to `exclude-paths` at the CLI level — `.agents/` is already excluded there. + +**Patterns to follow:** + +* The existing `.cursor-plugin/plugin.json` entries in the same `extra-files` arrays — this is a mechanical parallel addition + +**Test scenarios:** + +* Test expectation: none for the JSON file itself. Validator coverage in Unit 5 will exercise the updated config. + +**Verification:** + +* `bun run release:validate` still passes after this unit + +* release-please dry-run / preview (if available in the repo's CI) shows both Codex plugin.json files would be bumped on next release + +*** + +* [ ] **Unit 5: Extend release metadata sync + validator for Codex manifests** + +**Goal:** `bun run release:validate` cross-checks `.agents/plugins/marketplace.json` + `.codex-plugin/plugin.json` files against the Claude source of truth, failing on drift. + +**Requirements:** R4 + +**Dependencies:** Units 1, 2, 3 + +**Files:** + +* Modify: `src/release/components.ts` + +* Modify: `src/release/metadata.ts` + +* Modify: `scripts/release/validate.ts` (if the Codex manifests need to surface separately in the validate output; may be no-op if `syncReleaseMetadata` already drives everything) + +* Test: `tests/release-components.test.ts`, `tests/release-metadata.test.ts` (extend) + +**Approach:** + +* **`src/release/components.ts`:** declare any new file-path constants for Codex manifests. May or may not need a new "component" entry depending on how the sync engine is structured — the goal is that the sync engine knows where to find the Codex files, not that Codex gets its own release-please package. Follow the existing `.cursor-plugin/marketplace.json` / `.cursor-plugin` plugin pattern but omit marketplace-version tracking. + +* **`src/release/metadata.ts`:** extend `syncReleaseMetadata` to additionally: + + * Read `plugins/compound-engineering/.codex-plugin/plugin.json` and `plugins/coding-tutor/.codex-plugin/plugin.json` + + * Read `.agents/plugins/marketplace.json` + + * Cross-check: + + * Every plugin in `.claude-plugin/marketplace.json` has a corresponding entry in `.agents/plugins/marketplace.json` (same `name`) + + * For each plugin with both formats: `name` matches across `.claude-plugin/plugin.json` and `.codex-plugin/plugin.json` + + * For each plugin with both formats: `version` matches across the two plugin.json files (detect-only; release-please owns the write via Unit 4's `extra-files`) + + * For each plugin with both formats: `description` matches across `.claude-plugin/plugin.json` and `.codex-plugin/plugin.json` (mirrors the existing Claude ↔ Cursor description-sync rule in `src/release/metadata.ts`) + + * If `.codex-plugin/plugin.json` declares `skills: "./skills/"`, the directory `plugins//skills/` exists + + * Report drift via the existing `updates[]` mechanism (`changed: true` for detected name/version/description drift) + + * On `write: true`, rewrite `.codex-plugin/plugin.json` `description` to match Claude. **Do NOT rewrite `version`** — release-please owns version bumps via Unit 4's `extra-files` config, and having two authorities write the same field creates drift release-please can't reconcile. This mirrors the existing Cursor precedent: see the comment in `src/release/metadata.ts` ("Plugin versions are not synced in marketplace.json -- the canonical version lives in each plugin's own plugin.json. Duplicating versions here creates drift that release-please can't maintain."). + +* **`scripts/release/validate.ts`:** verify the output still prints a useful summary (may need to extend the success message to mention Codex counts; stretch goal, not required) + +**Patterns to follow:** + +* The existing Cursor integration in `src/release/metadata.ts` (around lines 138-230) — read both marketplaces, cross-check plugin lists and descriptions, update versions on write. Codex adds a parallel read + cross-check, minus the marketplace version update (Codex marketplace has no version field). + +**Test scenarios:** + +* Happy path: all manifests in sync, validator passes — add to `tests/release-metadata.test.ts` + +* Drift: Codex plugin.json version behind Claude plugin.json version, validator reports drift (NOT auto-corrected — release-please owns the bump) + +* Drift: Codex plugin.json `description` differs from Claude plugin.json `description`, write mode rewrites it to match + +* Drift: Codex marketplace missing a plugin that Claude has, validator reports drift + +* Drift: plugin `name` mismatches between Claude and Codex plugin.json, validator reports drift + +* Error path: `.codex-plugin/plugin.json` declares `skills: "./skills/"` but `plugins//skills/` doesn't exist, validator reports drift + +* Edge case: Codex marketplace has a plugin that Claude doesn't — validator reports drift (asymmetric additions rejected, since Claude is source of truth; this case is enumerated in the `metadata.ts` cross-check bullets above) + +**Verification:** + +* `bun test tests/release-metadata.test.ts` passes all new assertions + +* `bun run release:validate` returns success output on a clean working tree + +*** + +* [ ] **Unit 6: Update README with Codex native install flow** + +**Goal:** README documents the two-step Codex install (native plugin install for skills, Bun converter followup for agents). + +**Requirements:** R5 + +**Dependencies:** Units 1-3 (install commands reference the manifests; they must exist) + +**Files:** + +* Modify: `README.md` + +**Approach:** + +* Promote Codex out of the "experimental / Bun CLI" tier (line 129) into the native-install tier alongside Copilot/Droid/Qwen. + +* Add a new `### Codex` section with: + + * The native install command: `codex plugin marketplace add EveryInc/compound-engineering-plugin` + `codex plugin install compound-engineering` + + * A brief note that native install handles skills; for the full CE experience including agents, run the followup `bunx @every-env/compound-plugin install compound-engineering --to codex` + + * A cleanup pointer for users migrating from the old Bun-only install: `bunx @every-env/compound-plugin cleanup --target codex` (already exists) + +* Keep Codex in the Bun converter section too (line 129+) as an `--also` option for users who want a scripted install, but reframe: "the Bun converter remains the way to install CE's custom agents on Codex after the native plugin install." + +**Patterns to follow:** + +* The existing `### Factory Droid` and `### GitHub Copilot CLI` sections (lines \~85-110) — same shape: native install commands first, cleanup note, then any followup + +* `### Qwen Code` section — closest parallel since Qwen also migrated from Bun to native in this PR + +**Test scenarios:** + +* Test expectation: none -- documentation. Review for accuracy during implementation. + +**Verification:** + +* README lints / renders correctly + +* Install commands match what's declared in `.agents/plugins/marketplace.json` and the plugin name in `.codex-plugin/plugin.json` + +*** + +* [ ] **Unit 7: Empirical verification via local install** + +**Goal:** Confirm `codex plugin marketplace add ` + `codex plugin install compound-engineering` works end to end on the working tree before the branch is merged. + +**Requirements:** R1, R2, R6 + +**Dependencies:** Units 1-6 + +**Files:** + +* None (this unit is verification, not code) + +**Approach:** + +* On a clean Codex test environment (or with backups of existing `~/.codex/plugins/compound-engineering` and `~/.agents/skills/` state if present): + + 1. `codex plugin marketplace add ` — should succeed without the "marketplace file does not exist" error + 2. `codex plugin install compound-engineering` — should register the plugin and copy skills to the expected install location + 3. Inspect `~/.codex/plugins/compound-engineering/` (or wherever the install landed) — confirm CE skills are present. **Count assertion:** the installed skill count must match the source — CE ships 43 skill directories under `plugins/compound-engineering/skills/`; if fewer appear post-install, diagnose before proceeding (indicates Codex discovery isn't walking the layout CE uses, despite Vercel's reference plugin using the same pattern) + 4. Inspect `~/.agents/skills/` — confirm skills are discoverable by default trigger behavior + 5. Launch Codex and invoke a CE skill (e.g., `$ce-plan`) — should resolve and load + 6. `codex plugin uninstall compound-engineering` — confirm clean removal + 7. Smoke check for `coding-tutor`: `codex plugin install coding-tutor` succeeds and skills appear; do not run the full install/uninstall cycle — R2 targets `compound-engineering` only; `coding-tutor` is present for marketplace parity + +* If any step fails, diagnose via the error message and revise the relevant plugin.json or marketplace.json. Likely failure modes: + + * Required field we missed in plugin.json (fix: add it) + + * Schema mismatch on `source{}` or `policy{}` shape (fix: adjust) + + * Skill registration silent failure (fix: inspect Codex logs, add trigger metadata if needed — though this was decided out of scope, if empirically required we revisit) + +* Document any findings from this empirical test in the plan's `Open Questions` → `Deferred to Implementation` section as resolved. + +**Test scenarios:** + +* Happy path: native install succeeds, skills discoverable + +* Edge case: install + uninstall leaves no orphan state + +* Edge case: reinstall over existing install replaces cleanly + +* Integration: invoking an installed skill from Codex works + +**Verification:** + +* Successful install + uninstall cycle for `compound-engineering`; smoke-level install for `coding-tutor` + +* Skills invocable in Codex via default discovery; installed skill count matches the source + +* No new errors in Codex logs that weren't present before + +* **Merge gate:** Unit 7 must complete successfully before this PR merges. If empirical install fails, iterate on Units 1-3 manifests until install succeeds. Do not land Units 1-6 separately — the whole hybrid-install promise relies on native install actually working against these manifests, so a PR that ships the manifests untested would break CE's install story for any Codex user who follows the README. + + *** + + * [ ] **Unit 8: Update plugin AGENTS.md with Codex manifest contributor rules** + + **Goal:** Extend `plugins/compound-engineering/AGENTS.md` so contributors know the Codex manifests are release-owned (do not hand-bump) and know what to do when adding a new plugin (three-marketplace parity). + + **Requirements:** R3, R6 + + **Dependencies:** Units 1-5 (files must exist; validator must enforce the rules AGENTS.md describes — otherwise the doc describes an unenforced contract) + + **Files:** + + * Modify: `plugins/compound-engineering/AGENTS.md` + + **Approach:** + + * Extend the "Versioning Requirements → Contributor Rules" section with parallel Codex rules mirroring the existing Claude/Cursor ones: + + * Do NOT manually bump `.codex-plugin/plugin.json` version — release-please bumps it via `extra-files` in `.github/release-please-config.json` + + * Do NOT hand-edit `.agents/plugins/marketplace.json` except to add or remove a plugin (name, description, and plugin list drift are caught by `bun run release:validate`) + + * Extend the "Pre-Commit Checklist" with a parallel Codex entry: + + * `[ ] No manual release-version bump in .codex-plugin/plugin.json` + + * Add a brief "Adding a New Plugin" subsection (or extend "Adding Components") listing the three-marketplace parity requirement when a new plugin is added to the repo. Checklist items: entry in `.claude-plugin/marketplace.json`, entry in `.cursor-plugin/marketplace.json`, entry in `.agents/plugins/marketplace.json`, per-plugin `.claude-plugin/plugin.json` / `.cursor-plugin/plugin.json` / `.codex-plugin/plugin.json`, release-please config entry with all three `extra-files`, run `bun run release:validate` to confirm consistency. + + * Reference Unit 5 in the doc: the validator now enforces the rules described here, so a contributor who only touches one format will get a clear CI signal. + + **Patterns to follow:** + + * Existing "Versioning Requirements" and "Pre-Commit Checklist" sections in `plugins/compound-engineering/AGENTS.md` + + * Existing "Adding Components" section (currently covers skills + agents; extend or supplement with plugin-addition workflow) + + **Test scenarios:** + + * Test expectation: none -- documentation change. Implementer should verify by re-reading the extended sections and confirming they read as coherent parallels of the existing Claude/Cursor guidance. + + **Verification:** + + * AGENTS.md renders correctly; new sections integrate with existing structure + + * A contributor reading the Pre-Commit Checklist sees parallel rules for all three formats (Claude, Cursor, Codex) with matching language + + * A contributor adding a new plugin can follow the parity checklist without guessing which files to update + +*** + +* [ ] **Unit 9: Change `--to codex` default to agents-only + add `--include-skills` flag** + +**Goal:** Prevent skill double-registration when users run both Codex native plugin install AND the Bun converter. Make the Bun converter's `--to codex` default complement native install rather than duplicate it. + +**Requirements:** R2, R6 + +**Dependencies:** Units 1-3 (Codex manifests exist so native install actually registers skills). This unit assumes the two-step flow is the intended happy path. + +**Files:** +- Modify: `src/converters/claude-to-codex.ts` +- Modify: `src/converters/claude-to-opencode.ts` (add optional `codexIncludeSkills` field to the shared options type) +- Modify: `src/commands/install.ts` (add `--include-skills` flag + pass through) +- Modify: `src/commands/convert.ts` (same flag + pass through) +- Modify: `src/sync/commands.ts` (pin `codexIncludeSkills: true` on the legacy sync path — sync is not paired with native install and must continue emitting the full bundle) +- Test: `tests/codex-converter.test.ts` (add agents-only tests; update existing full-mode tests to pass the flag explicitly) +- Test: `tests/cli.test.ts` (new test for agents-only default; update existing `--to codex` tests to pass `--include-skills`) +- Modify: `README.md` (update the Codex install section to explain the new default + flag) + +**Approach:** +- Add `codexIncludeSkills?: boolean` to `ClaudeToOpenCodeOptions`. Document that it is Codex-only; other targets ignore it. +- In `convertClaudeToCodex`, default `includeSkills = options.codexIncludeSkills ?? false`. When false, return a bundle with empty `skillDirs`, empty `prompts`, empty command-skills, empty `mcpServers`; `generatedSkills` contains only agent conversions. When true, current full behavior. +- Agent bodies still get `transformContentForCodex` applied in both modes so `Task(...)` / slash refs rewrite against the skill graph that native install registers at runtime. +- CLI flag: `--include-skills` boolean, default false. Help text explicitly calls out that it is Codex-only, explains why (pairing with `codex plugin install`), and notes the flag's transience (will be unnecessary when Codex supports custom agents natively). +- `sync` command (legacy personal-config flow) pins the flag true — those users don't have native install as an option. +- Coding-tutor: no special-casing. With 0 agents, agents-only default emits an empty bundle — "bare minimum" per the product decision. Users wanting coding-tutor's commands run with `--include-skills`. + +**Patterns to follow:** +- The existing Cursor-specific option fields precedent in `ClaudeToOpenCodeOptions` (none currently, but the same field-on-shared-type pattern is used elsewhere for target-specific knobs) +- CLI flag description shape matching existing `inferTemperature` / `agentMode` entries + +**Test scenarios:** +- Happy path (agents-only default): bundle has empty `skillDirs`, empty `prompts`, `generatedSkills` contains only agent conversions, `mcpServers` undefined +- Happy path (`--include-skills`): existing tests continue to pass (full bundle emitted) +- Edge case: plugin with 0 agents produces an empty bundle in default mode (no orphan state, no possibility of conflict) +- Integration: agent body containing `Task x(...)` still gets rewritten in default mode (reference targets still populated from full plugin) +- CLI: `install --to codex` default writes agent files but NO `skills/ce-plan/SKILL.md` (assertion on file absence) +- CLI: `install --to codex --include-skills` writes the full tree (existing behavior preserved) +- Legacy path: `sync --target codex` still emits full bundle (codexIncludeSkills pinned true on that path) + +**Verification:** +- Existing Codex converter tests all pass with `codexIncludeSkills: true` added +- New agents-only tests pass +- `bun test` is green (no regressions elsewhere) +- README reflects the new default + opt-in flag + +*** + +## System-Wide Impact + +* **Interaction graph:** release-please now touches three plugin.json files per plugin per release (Claude, Cursor, Codex). `syncReleaseMetadata` now reads three marketplaces (Claude, Cursor, Codex). `bun run release:validate` now enforces tri-format consistency. + +* **Error propagation:** release validation drift now fails builds for Codex-specific mismatches too. This is a new failure mode CI will surface. Acceptable — same shape as the existing Cursor drift checks. + +* **State lifecycle risks:** none at runtime — this change ships static content (manifests) and release-time checks. No code paths change for users who only use the existing Claude/Cursor/Bun-converter flows. + +* **API surface parity:** native Codex plugin install is a new distribution surface; users upgrading from Bun-converter-installed CE to native-installed CE will have dual state briefly. The existing `cleanup --target codex` command already handles legacy CE state; documenting the migration in the README (Unit 6) should suffice. + +* **Integration coverage:** Unit 5 tests cross-format consistency. Unit 7 empirically validates the native install flow end-to-end. + +* **Unchanged invariants:** + + * Bun converter (`bunx ... --to codex`) continues to work unchanged — still writes agents to `~/.codex/agents/compound-engineering/` per existing logic + + * `cleanup --target codex` continues to work unchanged — managed-install manifest at `~/.codex/compound-engineering/install-manifest.json` still governs agent cleanup + + * Claude, Cursor, Copilot, Droid, Qwen install paths unchanged + + * `.claude-plugin/*` and `.cursor-plugin/*` files unchanged + + * No changes to `src/targets/codex.ts`, `src/converters/claude-to-codex.ts`, or any existing converter code — the Bun converter path stays whole for agents + +## Risks & Dependencies + +| Risk | Mitigation | +| :--------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Codex plugin.json requires a field we haven't identified from docs | Unit 7 empirical test catches this pre-merge; iterate on the manifest until install succeeds | +| Codex skills registration requires the `metadata.*` frontmatter extensions to work, not just `name`/`description` | Unit 7 empirical test catches this. If confirmed, escalate to user: either add minimal default metadata to CE skills (in scope), or accept degraded trigger behavior and defer full metadata tuning (deferred to later plan) | +| Release-please `extra-files` path change silently breaks version bump flow | Unit 5 validator catches drift *after* a release produces it — retroactive, not pre-merge. Before merging Unit 4, run release-please's preview/dry-run locally (`npx release-please manifest-pr --dry-run` or equivalent) and confirm both `.codex-plugin/plugin.json` files appear in the proposed bump list. AGENTS.md notes `linked-versions` has edge cases around `exclude-paths` — verify those don't interfere. | +| Skills that delegate to agents via `Task` silently fail on native-only install. CE skills like `ce-code-review`, `ce-plan`, `ce-work` spawn agents in `review/`, `research/`, `workflow/` subdirectories. Users who run native install and skip the `bunx ... --to codex` followup invoke those skills and see delegation failures that look like CE is broken. | Unit 6 README change is the primary mitigation (explicit two-step sequencing, with the agent followup called out as required for agent-heavy workflows). The `cleanup --target codex` command points users at the same CE namespace for a clean slate. **Followup plan to evaluate:** skill-side detection — delegating skills check for their required agents and emit a clear "run the agent followup to enable this" message when missing. Not in scope for this plan. Acceptable risk for the first release given the README is explicit. | +| User confusion about the two-step install (skills via native, agents via Bun) beyond the delegation failure above | Same README mitigation. If confusion is common post-launch, a followup plan automates the hybrid into a single command. | +| Codex marketplace schema evolves (OpenAI updates the spec) | Low probability in the short term; the worked-out schema matches both the bundled example and the canonical reference repo. Monitor Codex release notes; if `version` becomes required on marketplace.json, add it as an `extra-files` entry then | +| `coding-tutor`'s commands silently don't install and users don't notice | README explicitly calls this out in the coding-tutor install section. Acceptable gap — coding-tutor is lightly used and the commands gap is upstream (Codex spec limitation), not fixable in this repo | + +## Documentation / Operational Notes + +* README update is the main docs change (Unit 6) + +* No CHANGELOG entry needed — release-please will generate one based on commit messages (`feat(install):` or `feat(codex):` as the scope) + +* No rollout plan needed — this is pure additive content; users who don't use Codex are unaffected + +* Monitor post-merge: any issues opened about Codex install should be easy to triage (native install vs. Bun converter path makes the ownership clear) + +## Sources & References + +* Codex docs: [developers.openai.com/codex/plugins](https://developers.openai.com/codex/plugins), [/codex/plugins/build](https://developers.openai.com/codex/plugins/build) + +* Canonical reference: [github.com/openai/plugins](https://github.com/openai/plugins) + +* Local evidence: + + * `~/.codex/.tmp/bundled-marketplaces/openai-bundled/` — OpenAI bundled marketplace example + + * `~/.codex/.tmp/plugins/plugins/vercel/`, `~/.codex/.tmp/plugins/plugins/github/` — skills-declaring reference plugins + +* Related existing code: + + * `.github/release-please-config.json`, `src/release/metadata.ts`, `src/release/components.ts` + + * `.claude-plugin/marketplace.json`, `.cursor-plugin/marketplace.json` — prior-art dual-format precedent + +* Related PR: #609 (this branch) — the surrounding native-install-cleanup work diff --git a/plugins/coding-tutor/.codex-plugin/plugin.json b/plugins/coding-tutor/.codex-plugin/plugin.json new file mode 100644 index 0000000..4828d19 --- /dev/null +++ b/plugins/coding-tutor/.codex-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "coding-tutor", + "version": "1.2.1", + "description": "Personalized coding tutorials that use your actual codebase for examples with spaced repetition quizzes", + "author": { + "name": "Nityesh Agarwal" + }, + "license": "MIT", + "keywords": [ + "coding", + "programming", + "tutorial", + "learning", + "spaced-repetition" + ], + "skills": "./skills/", + "interface": { + "displayName": "Coding Tutor", + "shortDescription": "Personalized coding tutorials that use your actual codebase for examples with spaced repetition quizzes", + "longDescription": "Coding Tutor guides you through lessons that draw examples from the repo you're working in, then reinforces what you learned with spaced repetition quizzes. Skills install natively via Codex; Codex does not yet register plugin-declared commands, so the slash commands this plugin ships (e.g., quiz scheduling) require the companion Bun converter (see README).", + "developerName": "Nityesh Agarwal", + "category": "Coding", + "capabilities": [ + "Interactive", + "Read" + ], + "defaultPrompt": [ + "Teach me about the auth flow in this codebase", + "Quiz me on what I learned last week" + ], + "screenshots": [] + } +} diff --git a/plugins/compound-engineering/.codex-plugin/plugin.json b/plugins/compound-engineering/.codex-plugin/plugin.json new file mode 100644 index 0000000..d68bf9c --- /dev/null +++ b/plugins/compound-engineering/.codex-plugin/plugin.json @@ -0,0 +1,45 @@ +{ + "name": "compound-engineering", + "version": "2.68.1", + "description": "AI-powered development tools for code review, research, design, and workflow automation.", + "author": { + "name": "Kieran Klaassen", + "email": "kieran@every.to", + "url": "https://github.com/kieranklaassen" + }, + "homepage": "https://every.to/source-code/my-ai-had-already-fixed-the-code-before-i-saw-it", + "repository": "https://github.com/EveryInc/compound-engineering-plugin", + "license": "MIT", + "keywords": [ + "ai-powered", + "compound-engineering", + "workflow-automation", + "code-review", + "rails", + "ruby", + "python", + "typescript", + "knowledge-management", + "image-generation" + ], + "skills": "./skills/", + "interface": { + "displayName": "Compound Engineering", + "shortDescription": "AI-powered development tools for code review, research, design, and workflow automation.", + "longDescription": "Compound Engineering is a suite of skills and agents that make each unit of engineering work easier than the last. Brainstorm requirements, plan implementations, review code with specialized reviewers, research institutional learnings, and capture solved problems so future work benefits. Skills install natively via Codex; for the full experience with specialized review and research agents, run the companion Bun converter after install (see README).", + "developerName": "Every", + "category": "Coding", + "capabilities": [ + "Interactive", + "Read", + "Write" + ], + "websiteURL": "https://every.to/source-code/my-ai-had-already-fixed-the-code-before-i-saw-it", + "defaultPrompt": [ + "/ce-brainstorm a new feature", + "/ce-plan the implementation", + "/ce-code-review my changes" + ], + "screenshots": [] + } +} diff --git a/plugins/compound-engineering/AGENTS.md b/plugins/compound-engineering/AGENTS.md index a6025f5..bec4809 100644 --- a/plugins/compound-engineering/AGENTS.md +++ b/plugins/compound-engineering/AGENTS.md @@ -14,7 +14,10 @@ The repo uses an automated release process to prepare plugin releases, including ### Contributor Rules - Do **not** manually bump `.claude-plugin/plugin.json` version in a normal feature PR. +- Do **not** manually bump `.cursor-plugin/plugin.json` version in a normal feature PR. +- Do **not** manually bump `.codex-plugin/plugin.json` version in a normal feature PR — release-please owns this via `extra-files` in `.github/release-please-config.json`, parallel to the Claude and Cursor entries. - Do **not** manually bump `.claude-plugin/marketplace.json` plugin version in a normal feature PR. +- Do **not** hand-edit `.agents/plugins/marketplace.json` except to add or remove a plugin. Plugin-list, name, and description drift between the Claude, Cursor, and Codex marketplaces is caught by `bun run release:validate`. - Do **not** cut a release section in the canonical root `CHANGELOG.md` for a normal feature PR. - Do update substantive docs that are part of the actual change, such as `README.md`, component tables, usage instructions, or counts when they would otherwise become inaccurate. @@ -23,8 +26,11 @@ The repo uses an automated release process to prepare plugin releases, including Before committing ANY changes: - [ ] No manual release-version bump in `.claude-plugin/plugin.json` +- [ ] No manual release-version bump in `.cursor-plugin/plugin.json` +- [ ] No manual release-version bump in `.codex-plugin/plugin.json` - [ ] No manual release-version bump in `.claude-plugin/marketplace.json` - [ ] No manual release entry added to the root `CHANGELOG.md` +- [ ] `bun run release:validate` passes (enforces Claude/Cursor/Codex manifest parity) - [ ] README.md component counts verified - [ ] README.md tables accurate (agents, commands, skills) - [ ] plugin.json description matches current counts @@ -206,6 +212,23 @@ grep -E '^description:' skills/*/SKILL.md - **New skill:** Create `skills//SKILL.md` with required YAML frontmatter (`name`, `description`). Reference files go in `skills//references/`. Add the skill to the appropriate category table in `README.md` and update the skill count. - **New agent:** Create `agents//.md` with frontmatter. Categories: `review`, `document-review`, `research`, `design`, `docs`, `workflow`. Add the agent to `README.md` and update the agent count. +### Adding a New Plugin to This Repo + +When adding a new plugin alongside `compound-engineering` and `coding-tutor`, the repo ships to three marketplace formats (Claude, Cursor, Codex). All three must stay in parity or `bun run release:validate` will fail on next run. Checklist: + +- [ ] `.claude-plugin/marketplace.json` — add the plugin to `plugins[]` +- [ ] `.cursor-plugin/marketplace.json` — add the plugin to `plugins[]` +- [ ] `.agents/plugins/marketplace.json` — add the plugin to `plugins[]` (Codex schema: nested `source: { source: "local", path: "./plugins/" }`, `policy`, `category`) +- [ ] `plugins//.claude-plugin/plugin.json` — create with `name`, `version`, `description` +- [ ] `plugins//.cursor-plugin/plugin.json` — create with matching `name`, `version`, `description` +- [ ] `plugins//.codex-plugin/plugin.json` — create with matching `name`, `version`, `description`, plus Codex-specific fields (`skills: "./skills/"` if skills exist, plus `interface{}` block) +- [ ] `.github/release-please-config.json` — add a `plugins/` package entry with `extra-files` for all three plugin.json paths +- [ ] `.github/.release-please-manifest.json` — add the initial version entry for the new package +- [ ] `src/release/metadata.ts` — extend `syncReleaseMetadata` with a cross-check target for the new plugin (follow the `codexPluginTargets` pattern) +- [ ] Run `bun run release:validate` and confirm it reports the new manifests without drift + +The validator enforces: plugin-list parity across all three marketplaces, name/version/description parity across each plugin's three plugin.json files, and existence of any `skills:` directory declared in the Codex manifest. Note that only `description` drift is auto-corrected on `write: true` — version drift is detect-only because release-please owns the write. + ## Beta Skills Beta skills use a `-beta` suffix and `disable-model-invocation: true` to prevent accidental auto-triggering. See `docs/solutions/skill-design/beta-skills-framework.md` for naming, validation, and promotion rules. diff --git a/scripts/release/validate.ts b/scripts/release/validate.ts index 25bcbf6..c6845cc 100644 --- a/scripts/release/validate.ts +++ b/scripts/release/validate.ts @@ -22,8 +22,9 @@ const result = await syncReleaseMetadata({ }, }) const changed = result.updates.filter((update) => update.changed) +const metadataErrors = result.errors -if (configErrors.length === 0 && changed.length === 0) { +if (configErrors.length === 0 && changed.length === 0 && metadataErrors.length === 0) { console.log( `Release metadata is in sync. compound-engineering currently has ${counts.agents} agents, ${counts.skills} skills, and ${counts.mcpServers} MCP server${counts.mcpServers === 1 ? "" : "s"}.`, ) @@ -37,6 +38,13 @@ if (configErrors.length > 0) { } } +if (metadataErrors.length > 0) { + console.error("Release metadata structural errors detected:") + for (const error of metadataErrors) { + console.error(`- ${error}`) + } +} + if (changed.length > 0) { console.error("Release metadata drift detected:") for (const update of changed) { diff --git a/src/commands/cleanup.ts b/src/commands/cleanup.ts index 52703b3..bbf7571 100644 --- a/src/commands/cleanup.ts +++ b/src/commands/cleanup.ts @@ -24,6 +24,7 @@ import { } from "../data/plugin-legacy-artifacts" import { moveLegacyArtifactToBackup } from "../targets/managed-artifacts" import { isManagedCodexAgentsSymlink, readCodexInstallManifest, resolveCodexManagedRoots } from "../targets/codex" +import { classifyCodexLegacyPromptOwnership } from "../utils/legacy-cleanup" import { isSafeManagedPath, pathExists, readJson, sanitizePathName } from "../utils/files" import { resolveOpenCodeGlobalRoot } from "../utils/opencode-config" import { expandHome, resolveTargetHome } from "../utils/resolve-home" @@ -252,6 +253,11 @@ async function cleanupCodex(plugin: Awaited> agentMode: "subagent", inferTemperature: true, permissions: "none", + // Cleanup needs the FULL bundle (skills, command-skills, agents) to know + // what's "current" vs "legacy." The agents-only default of `--to codex` + // is wrong here; it would make cleanup think every existing skill is + // legacy and remove them. + codexIncludeSkills: true, }) const artifacts = getLegacyCodexArtifacts(bundle) const currentNamespacedSkills = new Set([ @@ -275,6 +281,19 @@ async function cleanupCodex(plugin: Awaited> } } for (const promptFile of artifacts.prompts) { + // Ownership gate: `~/.codex/prompts/` is a shared directory across plugins + // and user-authored prompts. A filename match against the historical CE + // allow-list is not a strong enough signal — a user who creates + // `~/.codex/prompts/ce-plan.md` for their own workflow would otherwise see + // it swept into `compound-engineering/legacy-backup/` on every cleanup run. + // Mirror the body + frontmatter check used by `cleanupStalePrompts` so + // install-time and standalone cleanup paths treat ownership identically. + // "unknown" (no fingerprint on record) falls through so fully-retired + // historical wrappers still get cleaned up. Manifest-driven migration + // below is already safe because it only touches files CE recorded writing. + const promptPath = path.join(codexRoot, "prompts", promptFile) + const ownership = await classifyCodexLegacyPromptOwnership(promptPath) + if (ownership === "foreign") continue moved += await moveIfExists(managedDir, "prompts", path.join(codexRoot, "prompts"), promptFile, "Codex") } @@ -336,6 +355,9 @@ async function cleanupCodexSharedAgents( agentMode: "subagent", inferTemperature: true, permissions: "none", + // Same reason as cleanupCodex: cleanup needs the full bundle to make + // current-vs-legacy decisions correctly. + codexIncludeSkills: true, }) const artifacts = getLegacyCodexArtifacts(bundle) const managedDir = path.join(agentsRoot, "compound-engineering") diff --git a/src/commands/convert.ts b/src/commands/convert.ts index 99e3d2e..bb9cfa2 100644 --- a/src/commands/convert.ts +++ b/src/commands/convert.ts @@ -65,6 +65,12 @@ export default defineCommand({ default: true, description: "Infer agent temperature from name/description", }, + includeSkills: { + type: "boolean", + default: false, + alias: "include-skills", + description: "For --to codex only: also emit skills and commands. Default is agents-only, the recommended pairing with `codex plugin install`. Set this flag for a legacy / standalone install without Codex native plugin install. Ignored by other targets.", + }, }, async run({ args }) { const targetName = String(args.to) @@ -84,6 +90,7 @@ export default defineCommand({ agentMode: String(args.agentMode) === "primary" ? "primary" : "subagent", inferTemperature: Boolean(args.inferTemperature), permissions: permissions as PermissionMode, + codexIncludeSkills: Boolean(args.includeSkills), } if (targetName === "all") { diff --git a/src/commands/install.ts b/src/commands/install.ts index bebd85f..22acac4 100644 --- a/src/commands/install.ts +++ b/src/commands/install.ts @@ -68,6 +68,12 @@ export default defineCommand({ default: true, description: "Infer agent temperature from name/description", }, + includeSkills: { + type: "boolean", + default: false, + alias: "include-skills", + description: "For --to codex only: also emit skills and commands. Default is agents-only, the recommended pairing with `codex plugin install`. Set this flag for a legacy / standalone install without Codex native plugin install. Ignored by other targets.", + }, branch: { type: "string", description: "Git branch to clone from (e.g. feat/new-agents)", @@ -95,6 +101,7 @@ export default defineCommand({ agentMode: String(args.agentMode) === "primary" ? "primary" : "subagent", inferTemperature: Boolean(args.inferTemperature), permissions: permissions as PermissionMode, + codexIncludeSkills: Boolean(args.includeSkills), } if (targetName === "all") { diff --git a/src/converters/claude-to-codex.ts b/src/converters/claude-to-codex.ts index 7fc0301..a998776 100644 --- a/src/converters/claude-to-codex.ts +++ b/src/converters/claude-to-codex.ts @@ -1,7 +1,7 @@ import fs, { type Dirent } from "fs" import path from "path" import { formatFrontmatter } from "../utils/frontmatter" -import { type ClaudeAgent, type ClaudeCommand, type ClaudePlugin, type ClaudeSkill, filterSkillsByPlatform } from "../types/claude" +import { type ClaudeAgent, type ClaudeCommand, type ClaudePlugin, filterSkillsByPlatform } from "../types/claude" import type { CodexAgent, CodexBundle, CodexGeneratedSkill, CodexGeneratedSkillSidecarDir } from "../types/codex" import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode" import { @@ -16,8 +16,16 @@ const CODEX_DESCRIPTION_MAX_LENGTH = 1024 export function convertClaudeToCodex( plugin: ClaudePlugin, - _options: ClaudeToCodexOptions, + options: ClaudeToCodexOptions, ): CodexBundle { + // Agents-only is the default for --to codex. Skills and commands are + // expected to install via Codex's native plugin flow (`codex plugin install`) + // which reads the plugin's .codex-plugin/plugin.json manifest. The Bun + // converter fills the one gap Codex's native spec leaves open: custom + // agents. Emitting skills too would double-register them — once from native + // install, once from this converter. + const includeSkills = options.codexIncludeSkills ?? false + const platformSkills = filterSkillsByPlatform(plugin.skills, "codex") const invocableCommands = plugin.commands.filter((command) => !command.disableModelInvocation) const applyCompoundWorkflowModel = shouldApplyCompoundWorkflowModel(plugin) @@ -57,10 +65,50 @@ export function convertClaudeToCodex( } } + // Agents are always converted to TOML custom agents regardless of mode — + // that's the whole point of --to codex. invocationTargets is populated from + // the full plugin so agent bodies can reference skills correctly; native + // install makes those skills discoverable at runtime. const agents = plugin.agents.map(convertAgent) const agentTargets = buildAgentTargets(plugin, agents) const invocationTargets: CodexInvocationTargets = { promptTargets, skillTargets, agentTargets } + if (!includeSkills) { + // Default: agents-only. Skills, prompts, command-skills, and MCP are + // suppressed so native plugin install is the sole source for those + // artifact types. + // + // Pass through current skill NAMES (not contents) so `writeCodexBundle` + // treats them as "current" and `cleanupLegacyAgentSkillDirs` doesn't + // move still-active skills under `.codex/skills///` into + // legacy-backup. Without this, re-running `install --to codex` after a + // native plugin install would sweep allow-listed names like `ce-plan` + // into backup because `currentSkills` (derived from skillDirs and + // generatedSkills) would be empty while the legacy allow-list still + // lists them. + // Mirror the skill-name set that full mode would emit via `skillDirs`: + // current skills plus the canonical rewrites of deprecated workflow + // aliases. Deduped via Set so the caller doesn't have to worry about + // overlap between `copiedSkills` names and `skillTargets` values. + const externallyManagedSkillNames = Array.from(new Set([ + ...copiedSkills.map((skill) => skill.name), + ...deprecatedWorkflowAliases + .map((alias) => toCanonicalWorkflowSkillName(alias.name)) + .filter((name): name is string => name !== null), + ])) + return { + pluginName: plugin.manifest.name, + prompts: [], + skillDirs: [], + generatedSkills: [], + agents, + invocationTargets, + mcpServers: undefined, + externallyManagedSkillNames, + } + } + + // Full / legacy / standalone mode: everything goes through the converter. const commandSkills: CodexGeneratedSkill[] = [] const prompts = invocableCommands.map((command) => { const promptName = commandPromptNames.get(command.name)! @@ -70,13 +118,11 @@ export function convertClaudeToCodex( return { name: promptName, content } }) - const generatedSkills = [...commandSkills] - return { pluginName: plugin.manifest.name, prompts, skillDirs, - generatedSkills, + generatedSkills: [...commandSkills], agents, invocationTargets, mcpServers: plugin.mcpServers, diff --git a/src/converters/claude-to-opencode.ts b/src/converters/claude-to-opencode.ts index 990d4a3..50b378f 100644 --- a/src/converters/claude-to-opencode.ts +++ b/src/converters/claude-to-opencode.ts @@ -21,6 +21,24 @@ export type ClaudeToOpenCodeOptions = { agentMode: "primary" | "subagent" inferTemperature: boolean permissions: PermissionMode + /** + * Codex-only option. Ignored by other targets. + * + * When false (default), `convertClaudeToCodex` emits only agent conversions. + * Skills and commands are expected to install via Codex's native plugin flow + * (`codex plugin install`), which the Bun converter complements rather than + * duplicates. Without this setting, running both native install and the Bun + * converter registers skills twice — once from the native plugin manifest, + * once from the converter output — creating conflicts. + * + * When true, the converter emits skills (copied as-is), commands (as prompts + * and generated skills), and agents together. Use when installing without + * Codex native plugin install (legacy / standalone flow). + * + * Obsolete once Codex's native plugin spec supports custom agents; at that + * point the entire `--to codex` converter path is expected to be deprecated. + */ + codexIncludeSkills?: boolean } const TOOL_MAP: Record = { diff --git a/src/release/metadata.ts b/src/release/metadata.ts index 5e5f19a..36e5d52 100644 --- a/src/release/metadata.ts +++ b/src/release/metadata.ts @@ -1,6 +1,6 @@ import { promises as fs } from "fs" import path from "path" -import { readJson, readText, writeJson, writeText } from "../utils/files" +import { readJson, writeJson } from "../utils/files" import type { ReleaseComponent } from "./types" type ClaudePluginManifest = { @@ -14,6 +14,13 @@ type CursorPluginManifest = { description?: string } +type CodexPluginManifest = { + name: string + version: string + description?: string + skills?: string +} + type MarketplaceManifest = { metadata: { version: string @@ -26,6 +33,13 @@ type MarketplaceManifest = { }> } +type CodexMarketplaceManifest = { + name: string + plugins: Array<{ + name: string + }> +} + type SyncOptions = { root?: string componentVersions?: Partial> @@ -39,6 +53,7 @@ type FileUpdate = { export type MetadataSyncResult = { updates: FileUpdate[] + errors: string[] } export type CompoundEngineeringCounts = { @@ -131,6 +146,7 @@ export async function syncReleaseMetadata(options: SyncOptions = {}): Promise = [ + { + claudePath: compoundClaudePath, + claude: compoundClaude, + codexPath: compoundCodexPath, + expectedName: "compound-engineering", + }, + { + claudePath: codingTutorClaudePath, + claude: codingTutorClaude, + codexPath: codingTutorCodexPath, + expectedName: "coding-tutor", + }, + ] + + for (const { claudePath, claude, codexPath, expectedName } of codexPluginTargets) { + let codex: CodexPluginManifest + try { + codex = await readJson(codexPath) + } catch (err: unknown) { + if ((err as NodeJS.ErrnoException).code === "ENOENT") { + errors.push(`${codexPath} is missing but ${claudePath} exists. Codex manifest parity required.`) + updates.push({ path: codexPath, changed: false }) + continue + } + throw err + } + + if (codex.name !== expectedName) { + errors.push(`${codexPath}: name "${codex.name}" does not match expected "${expectedName}"`) + } + + let codexChanged = false + + // Version: detect-only (release-please owns the write via extra-files). + if (codex.version !== claude.version) { + codexChanged = true + } + + // Description: write-enabled (same pattern as Claude/Cursor description sync). + if (claude.description !== undefined && codex.description !== claude.description) { + codex.description = claude.description + codexChanged = true + } + + // Skills declaration: required. Codex native install is the source of + // skills for each plugin (and `--to codex` defaults to agents-only), so a + // missing `skills` field silently produces a broken install with no skills + // registered. Enforce presence, then verify the directory exists. + if (codex.skills === undefined) { + errors.push(`${codexPath} (${expectedName}): missing required field "skills". Codex plugins must declare a skills path (e.g., "./skills/").`) + } else { + const pluginDir = path.dirname(path.dirname(codexPath)) + const skillsDir = path.resolve(pluginDir, codex.skills) + try { + const stat = await fs.stat(skillsDir) + if (!stat.isDirectory()) { + errors.push(`${codexPath} declares skills: "${codex.skills}" but ${skillsDir} is not a directory`) + } + } catch (err: unknown) { + if ((err as NodeJS.ErrnoException).code === "ENOENT") { + errors.push(`${codexPath} declares skills: "${codex.skills}" but ${skillsDir} does not exist`) + } else { + throw err + } + } + } + + updates.push({ path: codexPath, changed: codexChanged }) + if (write && codexChanged) await writeJson(codexPath, codex) + } + + // Codex marketplace: plugin-list parity with Claude marketplace. The Codex + // marketplace has no metadata.version field and is treated as static content + // (no release-please entry). Plugin list must mirror Claude exactly. + try { + const marketplaceCodex = await readJson(marketplaceCodexPath) + const claudeNames = [...marketplaceClaude.plugins.map((p) => p.name)].sort() + const codexNames = [...marketplaceCodex.plugins.map((p) => p.name)].sort() + if (claudeNames.join("|") !== codexNames.join("|")) { + errors.push( + `${marketplaceCodexPath}: plugin list [${codexNames.join(", ")}] does not match ${marketplaceClaudePath} [${claudeNames.join(", ")}]`, + ) + } + updates.push({ path: marketplaceCodexPath, changed: false }) + } catch (err: unknown) { + if ((err as NodeJS.ErrnoException).code === "ENOENT") { + errors.push(`${marketplaceCodexPath} is missing but ${marketplaceClaudePath} exists. Codex marketplace parity required.`) + updates.push({ path: marketplaceCodexPath, changed: false }) + } else { + throw err + } + } + + return { updates, errors } } diff --git a/src/targets/codex.ts b/src/targets/codex.ts index f49aea7..dac7ed8 100644 --- a/src/targets/codex.ts +++ b/src/targets/codex.ts @@ -5,6 +5,7 @@ import type { CodexBundle } from "../types/codex" import type { ClaudeMcpServer } from "../types/claude" import { transformContentForCodex } from "../utils/codex-content" import { getLegacyCodexArtifacts } from "../data/plugin-legacy-artifacts" +import { classifyCodexLegacyPromptOwnership } from "../utils/legacy-cleanup" const MANAGED_START_MARKER = "# BEGIN Compound Engineering plugin MCP -- do not edit this block" const MANAGED_END_MARKER = "# END Compound Engineering plugin MCP" @@ -49,10 +50,17 @@ export async function writeCodexBundle(outputRoot: string, bundle: CodexBundle): const skillsRoot = pluginName ? path.join(codexRoot, "skills", pluginName) : path.join(codexRoot, "skills") - const currentSkills = [ + // Include `externallyManagedSkillNames` so agents-only installs (default + // `--to codex`) treat skills installed via Codex's native plugin flow as + // "current" for cleanup purposes. Without this, `cleanupLegacyAgentSkillDirs` + // would see an empty `currentSkills` set and sweep allow-listed names such + // as `ce-plan` out of `.codex/skills//` into legacy-backup on every + // re-run of `install --to codex`. + const currentSkills = Array.from(new Set([ ...bundle.skillDirs.map((skill) => sanitizePathName(skill.name)), ...bundle.generatedSkills.map((skill) => sanitizePathName(skill.name)), - ] + ...(bundle.externallyManagedSkillNames ?? []).map((name) => sanitizePathName(name)), + ])) await cleanupRemovedSkills(skillsRoot, manifest, currentSkills) if (bundle.skillDirs.length > 0) { @@ -257,6 +265,18 @@ async function cleanupKnownLegacyCodexArtifacts(codexRoot: string, bundle: Codex for (const promptFile of legacyArtifacts.prompts) { const legacyPromptPath = path.join(codexRoot, "prompts", promptFile) + // Ownership gate: `~/.codex/prompts/` is a shared directory across plugins + // and user-authored prompts. A filename match against the legacy allow-list + // is not a strong enough signal to move a file — a user who creates + // `~/.codex/prompts/ce-plan.md` for their own workflow would otherwise see + // it swept into `compound-engineering/legacy-backup/` on every install. + // Mirror the body + frontmatter check used by the standalone + // `cleanupStalePrompts` helper. "unknown" (no fingerprint on record, e.g. + // fully-retired wrappers like `reproduce-bug.md`) falls through to the + // historical allow-list behavior — user collisions at those names are + // unlikely and a strict gate would strand genuinely-owned orphans. + const ownership = await classifyCodexLegacyPromptOwnership(legacyPromptPath) + if (ownership === "foreign") continue await moveLegacyArtifactToBackup(codexRoot, pluginName, "prompts", legacyPromptPath) } } diff --git a/src/types/codex.ts b/src/types/codex.ts index ebdf0c2..361e012 100644 --- a/src/types/codex.ts +++ b/src/types/codex.ts @@ -37,4 +37,13 @@ export type CodexBundle = { agents?: CodexAgent[] invocationTargets?: CodexInvocationTargets mcpServers?: Record + /** + * Names of skills CE owns in the Codex managed tree that are NOT written by + * this bundle. Used in agents-only installs (default `--to codex`) where + * skill contents are installed via Codex's native plugin flow, but cleanup + * still needs to recognize those skill names as "current" (and therefore + * not legacy) when re-running the install. Entries are sanitized skill + * names (same shape as `skillDirs[].name` after `sanitizePathName`). + */ + externallyManagedSkillNames?: string[] } diff --git a/src/utils/legacy-cleanup.ts b/src/utils/legacy-cleanup.ts index 127e2f9..da85f0f 100644 --- a/src/utils/legacy-cleanup.ts +++ b/src/utils/legacy-cleanup.ts @@ -667,3 +667,44 @@ export async function cleanupStalePrompts(promptsDir: string): Promise { } return removed } + +/** + * Ownership verdict for an individual Codex prompt file at a shared path like + * `~/.codex/prompts/.md`. Used by callers in the Codex install and + * standalone-cleanup paths to gate legacy-name allow-list moves before + * renaming a file into `compound-engineering/legacy-backup/`. + * + * Verdicts: + * - `"ce-owned"`: body + frontmatter fingerprint match a known + * compound-engineering prompt-wrapper shape. Safe to move. + * - `"foreign"`: we have a fingerprint on record for this filename and the + * file does NOT match it. A user or sibling plugin authored this file — + * leave it alone. `~/.codex/prompts/` is a cross-plugin directory, so a + * name-only match (e.g. `ce-plan.md`) is not a strong enough signal. + * - `"unknown"`: we have no fingerprint on record for this filename. This + * applies to historical prompt wrappers whose corresponding CE skill no + * longer ships (e.g. `reproduce-bug.md`, `report-bug.md`) — user + * collisions at those names are unlikely, and the historical allow-list + * was written specifically to clean them up. Callers may fall back to + * name-only cleanup in this case. + * + * Rationale for the three-way split: `LEGACY_PROMPT_CURRENT_SKILL_FOR_FILE` + * + `LEGACY_PROMPT_DESCRIPTION_ALIASES` only cover prompt filenames whose + * corresponding ce-* skill is still shipped. For names that are fully + * retired, we have no description to compare against, so a strict ownership + * gate would strand genuinely-owned orphan wrappers. Reporting `"unknown"` + * lets callers keep the historical allow-list behavior for those while still + * gating the realistic collision vectors. + */ +export type CodexPromptOwnership = "ce-owned" | "foreign" | "unknown" + +export async function classifyCodexLegacyPromptOwnership( + promptPath: string, +): Promise { + const fileName = path.basename(promptPath) + const { prompts } = await loadLegacyFingerprints() + const hasFingerprint = prompts.has(fileName) || fileName in LEGACY_PROMPT_DESCRIPTION_ALIASES + if (!hasFingerprint) return "unknown" + const ceOwned = await isLegacyPromptWrapper(promptPath, prompts.get(fileName)) + return ceOwned ? "ce-owned" : "foreign" +} diff --git a/tests/cli.test.ts b/tests/cli.test.ts index c3d02a4..02aa87e 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -1088,6 +1088,7 @@ describe("CLI", () => { "compound-engineering", "--to", "codex", + "--include-skills", ], { cwd: workspaceRoot, stdout: "pipe", @@ -1114,6 +1115,50 @@ describe("CLI", () => { expect(await exists(path.join(codexRoot, "AGENTS.md"))).toBe(true) }) + test("install --to codex default is agents-only (skills handled by native plugin install)", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-codex-agents-only-")) + const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-codex-agents-only-ws-")) + const projectRoot = path.join(import.meta.dir, "..") + const codexRoot = path.join(tempRoot, ".codex") + + const proc = Bun.spawn([ + "bun", + "run", + path.join(projectRoot, "src", "index.ts"), + "install", + "compound-engineering", + "--to", + "codex", + ], { + cwd: workspaceRoot, + stdout: "pipe", + stderr: "pipe", + env: { + ...process.env, + HOME: tempRoot, + COMPOUND_PLUGIN_GITHUB_SOURCE: "/definitely-not-a-valid-plugin-source", + }, + }) + + const exitCode = await proc.exited + const stdout = await new Response(proc.stdout).text() + const stderr = await new Response(proc.stderr).text() + + if (exitCode !== 0) { + throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`) + } + + expect(stdout).toContain("Installed compound-engineering") + // Default omits skills; they're expected from `codex plugin install`. + expect(await exists(path.join(codexRoot, "skills", "ce-plan", "SKILL.md"))).toBe(false) + // Agents still land (as generated skills for now — Codex's native plugin + // spec does not register custom agents, so the Bun converter fills the gap). + expect(await exists(path.join(codexRoot, "skills"))).toBe(true) + // AGENTS.md is emitted because --to codex always ensures a root AGENTS.md + // exists for Codex's discovery chain. + expect(await exists(path.join(codexRoot, "AGENTS.md"))).toBe(true) + }) + test("install by name ignores same-named local directory", async () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-shadow-")) const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-shadow-workspace-")) @@ -1285,6 +1330,7 @@ describe("CLI", () => { "codex", "--codex-home", codexRoot, + "--include-skills", ], { cwd: path.join(import.meta.dir, ".."), stdout: "pipe", @@ -1326,6 +1372,7 @@ describe("CLI", () => { codexRoot, "--output", tempRoot, + "--include-skills", ], { cwd: path.join(import.meta.dir, ".."), stdout: "pipe", @@ -1805,7 +1852,11 @@ describe("CLI", () => { expect(stdout).not.toContain("cursor") expect(await exists(path.join(tempHome, ".config", "opencode", "opencode.json"))).toBe(true) - expect(await exists(path.join(tempHome, ".codex", "skills", "compound-engineering", "skill-one", "SKILL.md"))).toBe(true) + // Codex `--to all` install uses the agents-only default — skills come from + // `codex plugin install`, not the Bun converter. Verify agents landed + // (the gap the converter fills) rather than skills (which the default suppresses). + expect(await exists(path.join(tempHome, ".codex", "agents", "compound-engineering", "security-sentinel.toml"))).toBe(true) + expect(await exists(path.join(tempHome, ".codex", "skills", "compound-engineering", "skill-one", "SKILL.md"))).toBe(false) expect(await exists(path.join(tempHome, ".pi", "agent", "skills", "skill-one", "SKILL.md"))).toBe(true) expect(await exists(path.join(tempCwd, ".gemini", "skills", "skill-one", "SKILL.md"))).toBe(true) expect(await exists(path.join(tempCwd, ".kiro", "skills", "skill-one", "SKILL.md"))).toBe(true) diff --git a/tests/codex-converter.test.ts b/tests/codex-converter.test.ts index 724718f..6079a0d 100644 --- a/tests/codex-converter.test.ts +++ b/tests/codex-converter.test.ts @@ -46,11 +46,59 @@ const fixturePlugin: ClaudePlugin = { } describe("convertClaudeToCodex", () => { + test("default (agents-only): emits only agent conversions, no skills or prompts or command-skills", () => { + const bundle = convertClaudeToCodex(fixturePlugin, { + agentMode: "subagent", + inferTemperature: false, + permissions: "none", + // codexIncludeSkills omitted -> defaults to false + }) + + // Native Codex plugin install handles skills, commands, and MCP via the + // .codex-plugin/plugin.json manifest. The Bun converter only fills the + // agent gap, so skillDirs / prompts / generatedSkills / mcpServers are + // all empty by default. + expect(bundle.skillDirs).toEqual([]) + expect(bundle.prompts).toEqual([]) + expect(bundle.generatedSkills).toEqual([]) + expect(bundle.mcpServers).toBeUndefined() + + // Custom agents (TOML) still land with instructions populated. + expect(bundle.agents).toHaveLength(1) + const agent = bundle.agents[0]! + expect(agent.name).toBe("security-reviewer") + expect(agent.description).toBe("Security-focused agent") + expect(agent.instructions).toContain("Focus on vulnerabilities.") + expect(agent.instructions).toContain("Threat modeling") + }) + + test("default with zero agents: emits fully empty bundle (no duplicate install possible)", () => { + const pluginWithNoAgents: ClaudePlugin = { + ...fixturePlugin, + agents: [], + } + const bundle = convertClaudeToCodex(pluginWithNoAgents, { + agentMode: "subagent", + inferTemperature: false, + permissions: "none", + }) + + expect(bundle.skillDirs).toEqual([]) + expect(bundle.prompts).toEqual([]) + expect(bundle.generatedSkills).toEqual([]) + expect(bundle.agents).toEqual([]) + expect(bundle.mcpServers).toBeUndefined() + // invocationTargets still populated so any future --include-skills call + // on the same plugin would have a consistent reference graph. + expect(bundle.invocationTargets).toBeDefined() + }) + test("converts commands to prompts and agents to custom agents", () => { const bundle = convertClaudeToCodex(fixturePlugin, { agentMode: "subagent", inferTemperature: false, permissions: "none", + codexIncludeSkills: true, }) expect(bundle.prompts).toHaveLength(1) @@ -101,6 +149,7 @@ describe("convertClaudeToCodex", () => { agentMode: "subagent", inferTemperature: false, permissions: "none", + codexIncludeSkills: true, }) const agent = bundle.agents.find((s) => s.name === "fast-agent") @@ -136,6 +185,7 @@ describe("convertClaudeToCodex", () => { agentMode: "subagent", inferTemperature: false, permissions: "none", + codexIncludeSkills: true, }) // No prompt wrappers for workflow skills — they're directly invocable as skills @@ -173,6 +223,7 @@ describe("convertClaudeToCodex", () => { agentMode: "subagent", inferTemperature: false, permissions: "none", + codexIncludeSkills: true, }) expect(bundle.prompts).toHaveLength(0) @@ -184,6 +235,7 @@ describe("convertClaudeToCodex", () => { agentMode: "subagent", inferTemperature: false, permissions: "none", + codexIncludeSkills: true, }) expect(bundle.mcpServers?.local?.command).toBe("echo") @@ -235,6 +287,7 @@ Task best-practices-researcher(topic)`, agentMode: "subagent", inferTemperature: false, permissions: "none", + codexIncludeSkills: true, }) const commandSkill = bundle.generatedSkills.find((s) => s.name === "plan") @@ -295,6 +348,7 @@ Task compound-engineering:review:ce-security-reviewer(code_diff)`, agentMode: "subagent", inferTemperature: false, permissions: "none", + codexIncludeSkills: true, }) const commandSkill = bundle.generatedSkills.find((s) => s.name === "plan") @@ -335,6 +389,7 @@ Task compound-engineering:review:ce-security-reviewer(code_diff)`, agentMode: "subagent", inferTemperature: false, permissions: "none", + codexIncludeSkills: true, }) const commandSkill = bundle.generatedSkills.find((s) => s.name === "review") @@ -370,6 +425,7 @@ Don't confuse with file paths like /tmp/output.md or /dev/null.`, agentMode: "subagent", inferTemperature: false, permissions: "none", + codexIncludeSkills: true, }) const commandSkill = bundle.generatedSkills.find((s) => s.name === "plan") @@ -413,6 +469,7 @@ Don't confuse with file paths like /tmp/output.md or /dev/null.`, agentMode: "subagent", inferTemperature: false, permissions: "none", + codexIncludeSkills: true, }) const agent = bundle.agents.find((s) => s.name === "research-session-historian") @@ -469,6 +526,7 @@ If planning is complete, continue with /ce-work.`, agentMode: "subagent", inferTemperature: false, permissions: "none", + codexIncludeSkills: true, }) const commandSkill = bundle.generatedSkills.find((s) => s.name === "review") @@ -506,6 +564,7 @@ If planning is complete, continue with /ce-work.`, agentMode: "subagent", inferTemperature: false, permissions: "none", + codexIncludeSkills: true, }) // Only normal command should produce a prompt @@ -541,6 +600,7 @@ Run \`/compound-engineering-setup\` to create a settings file.`, agentMode: "subagent", inferTemperature: false, permissions: "none", + codexIncludeSkills: true, }) const commandSkill = bundle.generatedSkills.find((s) => s.name === "review") @@ -570,6 +630,7 @@ Run \`/compound-engineering-setup\` to create a settings file.`, agentMode: "subagent", inferTemperature: false, permissions: "none", + codexIncludeSkills: true, }) const agent = bundle.agents.find((s) => s.name === "config-reader") @@ -597,6 +658,7 @@ Run \`/compound-engineering-setup\` to create a settings file.`, agentMode: "subagent", inferTemperature: false, permissions: "none", + codexIncludeSkills: true, }) const description = bundle.agents[0].description diff --git a/tests/codex-writer.test.ts b/tests/codex-writer.test.ts index ff46e72..9cd585c 100644 --- a/tests/codex-writer.test.ts +++ b/tests/codex-writer.test.ts @@ -178,6 +178,47 @@ describe("writeCodexBundle", () => { expect(await exists(path.join(promptsDir, "ce-plan.md"))).toBe(true) }) + test("preserves same-named user prompts when pluginName triggers legacy allow-list cleanup", async () => { + // Regression: `cleanupKnownLegacyCodexArtifacts` used to move any + // allow-listed filename under `~/.codex/prompts/` into + // `compound-engineering/legacy-backup/` whenever `pluginName` was set, + // without checking that CE authored the file. A user-authored + // `ce-plan.md` prompt was therefore destroyed on `install --to codex` + // even though the content was not a CE-emitted wrapper. The install path + // now requires the same body + frontmatter ownership fingerprint that + // the standalone `cleanupStalePrompts` helper uses before touching a + // prompt file at a colliding legacy name. + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-prompts-legacy-preserve-")) + const codexRoot = path.join(tempRoot, ".codex") + const promptsDir = path.join(codexRoot, "prompts") + await fs.mkdir(promptsDir, { recursive: true }) + const userPromptBody = + "---\ndescription: \"Project-local ce-plan helper\"\n---\n\nCustom prompt body\n" + await fs.writeFile(path.join(promptsDir, "ce-plan.md"), userPromptBody) + + await writeCodexBundle(codexRoot, { + pluginName: "compound-engineering", + prompts: [], + skillDirs: [], + generatedSkills: [], + }) + + expect(await exists(path.join(promptsDir, "ce-plan.md"))).toBe(true) + expect(await fs.readFile(path.join(promptsDir, "ce-plan.md"), "utf8")).toBe(userPromptBody) + const backupRoot = path.join(codexRoot, "compound-engineering", "legacy-backup") + // The legacy-backup directory should not contain the user-authored prompt. + if (await exists(backupRoot)) { + const timestamps = await fs.readdir(backupRoot) + for (const timestamp of timestamps) { + const promptsBackup = path.join(backupRoot, timestamp, "prompts") + if (await exists(promptsBackup)) { + const backedUp = await fs.readdir(promptsBackup) + expect(backedUp).not.toContain("ce-plan.md") + } + } + } + }) + test("writes plugin skills under a namespaced Codex skills root without .agents symlinks", async () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-managed-plugin-")) const codexRoot = path.join(tempRoot, ".codex") @@ -354,6 +395,65 @@ describe("writeCodexBundle", () => { } }) + test("agents-only install preserves namespaced skills previously installed via Codex native plugin flow", async () => { + // Regression for the bug where re-running `install --to codex` after a + // native `/plugins` install moved currently-active namespaced skills + // (e.g., `.codex/skills/compound-engineering/ce-plan/`) into + // legacy-backup. The agents-only default produces an empty `skillDirs` / + // `generatedSkills`, but the converter now populates + // `externallyManagedSkillNames` with the allow-listed current skills so + // `cleanupLegacyAgentSkillDirs` treats them as current rather than legacy. + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-agents-only-preserve-")) + const codexRoot = path.join(tempRoot, ".codex") + + // Simulate the tree produced by a native Codex plugin install: active + // namespaced skills under `.codex/skills///SKILL.md`. + const namespacedSkillsRoot = path.join(codexRoot, "skills", "compound-engineering") + for (const skillName of ["ce-plan", "ce-debug", "ce-brainstorm"]) { + await fs.mkdir(path.join(namespacedSkillsRoot, skillName), { recursive: true }) + await fs.writeFile( + path.join(namespacedSkillsRoot, skillName, "SKILL.md"), + `# ${skillName} skill installed via native Codex plugin flow`, + ) + } + + const plugin = await loadClaudePlugin(path.join(import.meta.dir, "..", "plugins", "compound-engineering")) + const bundle = convertClaudeToCodex(plugin, { + agentMode: "subagent", + inferTemperature: true, + permissions: "none", + // codexIncludeSkills omitted -> agents-only default + }) + + // Sanity: agents-only bundle does not request any skill writes, but it + // does advertise the current skill names so cleanup preserves them. + expect(bundle.skillDirs).toEqual([]) + expect(bundle.generatedSkills).toEqual([]) + expect(bundle.externallyManagedSkillNames).toContain("ce-plan") + expect(bundle.externallyManagedSkillNames).toContain("ce-debug") + + await writeCodexBundle(codexRoot, bundle) + + // Currently-active skills survive an agents-only re-install. + expect(await exists(path.join(namespacedSkillsRoot, "ce-plan", "SKILL.md"))).toBe(true) + expect(await exists(path.join(namespacedSkillsRoot, "ce-debug", "SKILL.md"))).toBe(true) + expect(await exists(path.join(namespacedSkillsRoot, "ce-brainstorm", "SKILL.md"))).toBe(true) + + // And none of them were silently relocated into legacy-backup. + const backupRoot = path.join(codexRoot, "compound-engineering", "legacy-backup") + if (await exists(backupRoot)) { + const timestamps = await fs.readdir(backupRoot) + for (const ts of timestamps) { + const skillsBackup = path.join(backupRoot, ts, "skills") + if (!(await exists(skillsBackup))) continue + const backed = await fs.readdir(skillsBackup) + expect(backed).not.toContain("ce-plan") + expect(backed).not.toContain("ce-debug") + expect(backed).not.toContain("ce-brainstorm") + } + } + }) + test("preserves existing user config when writing MCP servers", async () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-backup-")) const codexRoot = path.join(tempRoot, ".codex") diff --git a/tests/release-metadata.test.ts b/tests/release-metadata.test.ts index 6227e1d..6a1508c 100644 --- a/tests/release-metadata.test.ts +++ b/tests/release-metadata.test.ts @@ -32,14 +32,24 @@ async function makeFixtureRoot(): Promise { await mkdir(path.join(root, "plugins", "compound-engineering", ".cursor-plugin"), { recursive: true, }) + await mkdir(path.join(root, "plugins", "compound-engineering", ".codex-plugin"), { + recursive: true, + }) + await mkdir(path.join(root, "plugins", "coding-tutor", "skills", "coding-tutor"), { + recursive: true, + }) await mkdir(path.join(root, "plugins", "coding-tutor", ".claude-plugin"), { recursive: true, }) await mkdir(path.join(root, "plugins", "coding-tutor", ".cursor-plugin"), { recursive: true, }) + await mkdir(path.join(root, "plugins", "coding-tutor", ".codex-plugin"), { + recursive: true, + }) await mkdir(path.join(root, ".claude-plugin"), { recursive: true }) await mkdir(path.join(root, ".cursor-plugin"), { recursive: true }) + await mkdir(path.join(root, ".agents", "plugins"), { recursive: true }) await writeFile( path.join(root, "plugins", "compound-engineering", "agents", "review", "agent.md"), @@ -69,6 +79,38 @@ async function makeFixtureRoot(): Promise { path.join(root, "plugins", "coding-tutor", ".cursor-plugin", "plugin.json"), JSON.stringify({ version: "1.2.1" }, null, 2), ) + await writeFile( + path.join(root, "plugins", "compound-engineering", ".codex-plugin", "plugin.json"), + JSON.stringify( + { + name: "compound-engineering", + version: "2.42.0", + description: "old", + skills: "./skills/", + }, + null, + 2, + ), + ) + await writeFile( + path.join(root, "plugins", "coding-tutor", ".codex-plugin", "plugin.json"), + JSON.stringify( + { name: "coding-tutor", version: "1.2.1", skills: "./skills/" }, + null, + 2, + ), + ) + await writeFile( + path.join(root, ".agents", "plugins", "marketplace.json"), + JSON.stringify( + { + name: "compound-engineering-plugin", + plugins: [{ name: "compound-engineering" }, { name: "coding-tutor" }], + }, + null, + 2, + ), + ) await writeFile( path.join(root, ".claude-plugin", "marketplace.json"), JSON.stringify( @@ -132,4 +174,202 @@ describe("release metadata", () => { expect(changedPaths).toContain(path.join(root, ".claude-plugin", "marketplace.json")) expect(changedPaths).toContain(path.join(root, ".cursor-plugin", "marketplace.json")) }) + + test("reports Codex plugin.json version drift without auto-correcting", async () => { + const root = await makeFixtureRoot() + // Claude is at 2.42.0; fixture Codex is also 2.42.0 — drift Codex to 2.41.0. + await writeFile( + path.join(root, "plugins", "compound-engineering", ".codex-plugin", "plugin.json"), + JSON.stringify( + { name: "compound-engineering", version: "2.41.0", skills: "./skills/" }, + null, + 2, + ), + ) + const result = await syncReleaseMetadata({ root, write: true }) + const codexPath = path.join(root, "plugins", "compound-engineering", ".codex-plugin", "plugin.json") + const codexUpdate = result.updates.find((u) => u.path === codexPath) + + expect(codexUpdate).toBeDefined() + expect(codexUpdate!.changed).toBe(true) + + // Crucially: write: true did NOT bump the Codex version to match Claude. + // release-please owns version writes via extra-files; syncReleaseMetadata detects but does not correct. + const afterContents = JSON.parse(await Bun.file(codexPath).text()) + expect(afterContents.version).toBe("2.41.0") + }) + + test("rewrites Codex plugin.json description on write when drifted from Claude", async () => { + const root = await makeFixtureRoot() + // Fixture Claude description is "old"; Codex starts at "old" too. Give Claude a canonical description and drift Codex. + await writeFile( + path.join(root, "plugins", "compound-engineering", ".claude-plugin", "plugin.json"), + JSON.stringify( + { + version: "2.42.0", + description: "AI-powered development tools for code review, research, design, and workflow automation.", + }, + null, + 2, + ), + ) + await writeFile( + path.join(root, "plugins", "compound-engineering", ".codex-plugin", "plugin.json"), + JSON.stringify( + { + name: "compound-engineering", + version: "2.42.0", + description: "stale codex description", + skills: "./skills/", + }, + null, + 2, + ), + ) + const codexPath = path.join(root, "plugins", "compound-engineering", ".codex-plugin", "plugin.json") + await syncReleaseMetadata({ root, write: true }) + + const afterContents = JSON.parse(await Bun.file(codexPath).text()) + expect(afterContents.description).toBe( + "AI-powered development tools for code review, research, design, and workflow automation.", + ) + }) + + test("reports missing Codex manifest as a structural error", async () => { + const root = await makeFixtureRoot() + await Bun.$`rm ${path.join(root, "plugins", "compound-engineering", ".codex-plugin", "plugin.json")}`.quiet() + + const result = await syncReleaseMetadata({ root, write: false }) + + expect(result.errors.some((err) => err.includes(".codex-plugin/plugin.json is missing"))).toBe(true) + }) + + test("reports Codex plugin.json name mismatch as structural error", async () => { + const root = await makeFixtureRoot() + await writeFile( + path.join(root, "plugins", "compound-engineering", ".codex-plugin", "plugin.json"), + JSON.stringify( + { name: "wrong-name", version: "2.42.0", skills: "./skills/" }, + null, + 2, + ), + ) + const result = await syncReleaseMetadata({ root, write: false }) + + expect( + result.errors.some((err) => + err.includes('name "wrong-name" does not match expected "compound-engineering"'), + ), + ).toBe(true) + }) + + test("reports missing skills field on Codex manifest as structural error", async () => { + const root = await makeFixtureRoot() + // Drop the `skills` field entirely from the coding-tutor Codex manifest. + await writeFile( + path.join(root, "plugins", "coding-tutor", ".codex-plugin", "plugin.json"), + JSON.stringify({ name: "coding-tutor", version: "1.2.1" }, null, 2), + ) + const result = await syncReleaseMetadata({ root, write: false }) + + expect( + result.errors.some( + (err) => + err.includes("coding-tutor") && + err.includes("missing required field") && + err.includes("skills"), + ), + ).toBe(true) + }) + + test("reports missing skills directory when Codex manifest declares one", async () => { + const root = await makeFixtureRoot() + // Remove coding-tutor's skills dir but keep the skills declaration. + await Bun.$`rm -rf ${path.join(root, "plugins", "coding-tutor", "skills")}`.quiet() + const result = await syncReleaseMetadata({ root, write: false }) + + expect( + result.errors.some( + (err) => + err.includes("coding-tutor") && err.includes("skills:") && err.includes("does not exist"), + ), + ).toBe(true) + }) + + test("reports Codex marketplace plugin-list mismatch as structural error", async () => { + const root = await makeFixtureRoot() + // Remove one plugin from Codex marketplace so Claude has a plugin Codex doesn't. + await writeFile( + path.join(root, ".agents", "plugins", "marketplace.json"), + JSON.stringify( + { + name: "compound-engineering-plugin", + plugins: [{ name: "compound-engineering" }], + }, + null, + 2, + ), + ) + const result = await syncReleaseMetadata({ root, write: false }) + + expect( + result.errors.some( + (err) => err.includes(".agents/plugins/marketplace.json") && err.includes("does not match"), + ), + ).toBe(true) + }) + + test("reports Codex marketplace asymmetric extra plugin as structural error", async () => { + const root = await makeFixtureRoot() + await writeFile( + path.join(root, ".agents", "plugins", "marketplace.json"), + JSON.stringify( + { + name: "compound-engineering-plugin", + plugins: [ + { name: "compound-engineering" }, + { name: "coding-tutor" }, + { name: "rogue-plugin" }, + ], + }, + null, + 2, + ), + ) + const result = await syncReleaseMetadata({ root, write: false }) + + expect( + result.errors.some( + (err) => err.includes(".agents/plugins/marketplace.json") && err.includes("does not match"), + ), + ).toBe(true) + }) + + test("happy path: fixture with matching Codex manifests produces no Codex errors", async () => { + const root = await makeFixtureRoot() + // Align Claude <-> Codex versions and descriptions so there's no drift. + await writeFile( + path.join(root, "plugins", "compound-engineering", ".claude-plugin", "plugin.json"), + JSON.stringify({ version: "2.42.0", description: "aligned description" }, null, 2), + ) + await writeFile( + path.join(root, "plugins", "compound-engineering", ".codex-plugin", "plugin.json"), + JSON.stringify( + { + name: "compound-engineering", + version: "2.42.0", + description: "aligned description", + skills: "./skills/", + }, + null, + 2, + ), + ) + + const result = await syncReleaseMetadata({ root, write: false }) + const codexErrors = result.errors.filter( + (err) => err.includes(".codex-plugin") || err.includes(".agents/plugins"), + ) + expect(codexErrors).toEqual([]) + }) })