diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..63b6dbc --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,31 @@ +{ + "name": "filip-podstavec", + "interface": { + "displayName": "Filip Podstavec" + }, + "plugins": [ + { + "name": "claude-leverage", + "version": "1.12.0", + "description": "Personal Claude Code + Codex dev stack: security hooks, AI-first code conventions, 14 on-demand skills (incl. /repo-doctor — AI-readiness audit with code↔docs drift detection — and /refresh-context-map for the v1.8.0 smart-context-surfacing hook), ADR + session-log conventions, portable statusline. Complements other skills-based plugins, not a replacement.", + "source": { + "source": "url", + "url": "https://github.com/Filip-Podstavec/claude-leverage.git" + }, + "policy": { + "installation": "AVAILABLE" + }, + "category": "workflow", + "keywords": [ + "ai-first", + "security-review", + "codex", + "claude-code", + "agents-md", + "repo-map", + "statusline", + "stack-freshness" + ] + } + ] +} diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index b603fe5..a42ac87 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -12,7 +12,7 @@ "url": "https://github.com/Filip-Podstavec/claude-leverage.git" }, "description": "Personal Claude Code + Codex dev stack: security hooks, AI-first code conventions, 14 on-demand skills (incl. /repo-doctor — AI-readiness audit with code↔docs drift detection — and /refresh-context-map for the v1.8.0 smart-context-surfacing hook), ADR + session-log conventions, portable statusline. Complements other skills-based plugins, not a replacement.", - "version": "1.11.0", + "version": "1.12.0", "category": "workflow", "keywords": [ "ai-first", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 3460379..d242b7f 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "claude-leverage", - "version": "1.11.0", + "version": "1.12.0", "description": "Personal Claude Code + Codex dev stack: security hooks, AI-first code conventions, /security-review, /repo-map, /stack-check, portable statusline. Designed to complement other skills-based plugins, not replace them.", "author": { "name": "Filip Podstavec", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json new file mode 100644 index 0000000..4c12f81 --- /dev/null +++ b/.codex-plugin/plugin.json @@ -0,0 +1,24 @@ +{ + "name": "claude-leverage", + "version": "1.12.0", + "description": "Personal Claude Code + Codex dev stack: security hooks, AI-first code conventions, /security-review, /repo-map, /stack-check, portable statusline. Designed to complement other skills-based plugins, not replace them.", + "author": { + "name": "Filip Podstavec", + "url": "https://github.com/Filip-Podstavec" + }, + "homepage": "https://github.com/Filip-Podstavec/claude-leverage", + "repository": "https://github.com/Filip-Podstavec/claude-leverage", + "license": "MIT", + "keywords": [ + "ai-first", + "security-review", + "codex", + "claude-code", + "agents-md", + "repo-map", + "statusline", + "stack-freshness" + ], + "skills": "./skills/", + "hooks": "./hooks/hooks.json" +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 25dd2a8..13a0cfc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,3 +77,14 @@ jobs: python-version: "3.11" - name: Check generator parity (gen-codex-agents.py --check) run: python scripts/gen-codex-agents.py --check + + codex-plugin-parity: + name: Codex plugin artifacts match Claude manifest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Check generator parity (gen-codex-plugin.py --check) + run: python scripts/gen-codex-plugin.py --check diff --git a/CHANGELOG.md b/CHANGELOG.md index e13fde2..b689d27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to `claude-leverage` are recorded here. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) + [SemVer](https://semver.org/spec/v2.0.0.html). +## [1.12.0] — 2026-06-04 + +### Added + +- **Codex plugin distribution path** — `.codex-plugin/plugin.json` and + `.agents/plugins/marketplace.json` are now **generated from the canonical + Claude manifest** by `scripts/gen-codex-plugin.py`, so Codex installs the same + plugin via its marketplace path without a hand-maintained second source of + truth. CI enforces artifact parity (`gen-codex-plugin.py --check`) and the + pre-push smoke run regenerates on drift. Rationale in + [ADR 0011](docs/adr/0011-codex-plugin-marketplace-third-distribution-path.md); + `docs/maintaining.md` gains the regenerate-on-change step. + ## [1.11.0] — 2026-06-03 ### Added @@ -484,7 +497,7 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) + ### Fixed (field-feedback bundle) - **`ai-first-nudge.sh`** split basename-only vs path ignore patterns - so directories named e.g. `slevomat_test_api/` no longer silently + so directories named e.g. `acme_test_api/` no longer silently swallow nudges for production files inside them. Added Windows backslash → forward slash normalization before pattern matching so `node_modules` and friends still match on Git Bash. diff --git a/README.md b/README.md index 93f9b0d..bc4abf4 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,25 @@ current session) to pick up all 15 skills and 2 subagents. ### Codex CLI -Codex has no plugin marketplace, so installation is via a script: +Two ways to use the stack with Codex — they're complementary, not either/or. + +**A. Plugin marketplace (skills + hooks).** Codex now has a plugin marketplace; +add this repo as a marketplace source and install it from `/plugins`: + +```bash +npm i -g @openai/codex # one time +``` +Then in a Codex session, open `/plugins`, add the marketplace from GitHub +shorthand `Filip-Podstavec/claude-leverage`, open the `claude-leverage` plugin, +and select **Install plugin**. + +This delivers the **skills** and the **security/nudge hooks** (Codex sets +`CLAUDE_PLUGIN_ROOT` for compatibility, so the same `hooks/hooks.json` works). +It does **not** deliver the subagents or the `/flaky-test` command — Codex +plugins don't load those — nor the global `@AGENTS.md` import. For the full +stack, use option B. + +**B. Install script (full stack: + subagents + global AGENTS.md import).** ```bash # 1. Install Codex CLI itself (one time) diff --git a/docs/adr/0011-codex-plugin-marketplace-third-distribution-path.md b/docs/adr/0011-codex-plugin-marketplace-third-distribution-path.md new file mode 100644 index 0000000..b9f1bcd --- /dev/null +++ b/docs/adr/0011-codex-plugin-marketplace-third-distribution-path.md @@ -0,0 +1,92 @@ +--- +status: accepted +date: 2026-06-03 +deciders: Filip Podstavec +consulted: Claude Opus 4.8 (brainstorming session) +informed: stack users +--- + +# 0011. Codex plugin marketplace as a third distribution path + +## Context and Problem Statement + +OpenAI Codex shipped a plugin marketplace (installable from GitHub shorthand, a +Git URL, or a local marketplace root). Its format is close to Claude Code's: +skills as `skills//SKILL.md`, hooks as `hooks/hooks.json`, a plugin +manifest, and a marketplace root — and Codex even sets `CLAUDE_PLUGIN_ROOT` for +hook compatibility. Until now this repo reached Codex two ways: Codex reads +`AGENTS.md` natively, and `scripts/install-codex.sh` copies skills/hooks/agents +into `~/.codex` + `~/.agents`. The question: should the stack also ship as an +installable Codex plugin, and if so, how do we keep yet another manifest from +drifting? + +Two format gaps block a drop-in install: Codex reads the plugin manifest ONLY +from `.codex-plugin/plugin.json` (no legacy fallback), and its marketplace +schema differs from Claude's (`interface.displayName` not `owner`; per-plugin +`policy`). + +## Decision + +We ship a Codex plugin distribution path via **generated** tool-native +artifacts. `scripts/gen-codex-plugin.py` derives `.codex-plugin/plugin.json` and +`.agents/plugins/marketplace.json` from the Claude manifest pair, which stays +the single source of truth. A `--check` mode runs in CI and `smoke-plugin.sh`. +The plugin path is **complementary** to the install script: it carries skills + +hooks only; subagents, the `/flaky-test` command, and the global `@AGENTS.md` +import remain on the script path. + +## Decision Drivers + +- The repo already treats Codex parity as a generated-and-`--check`ed artifact + (`gen-codex-agents.py`); a second generator is the idiomatic fit. +- Three manifests bumped by hand drift silently — exactly the failure + `check_version_sync.py` exists to stop. +- Codex plugins can't carry slash commands or subagents, so the plugin path + cannot fully replace the install script; framing them as complementary is + honest and avoids a regression. + +## Considered Options + +1. **Generated tool-native artifacts (selected).** New generator emits + `.codex-plugin/plugin.json` + `.agents/plugins/marketplace.json` from the + Claude source, `--check` in CI. Clean schema separation, no drift. +2. **Augment the shared `.claude-plugin/marketplace.json`** with Codex fields + (Codex reads it via the legacy path). Rejected: mixes two schemas in one + file and still needs a hand-written `.codex-plugin/plugin.json`. +3. **Hand-write both Codex files + extend `check_version_sync.py`.** Rejected: + more manual upkeep per change and weaker than generation against structural + drift. + +## Decision Outcome + +**Chosen: Option 1.** `.claude-plugin/` is the source of truth; the Codex +artifacts are generated and committed, guarded by `gen-codex-plugin.py --check` +in CI (`codex-plugin-parity` job) and `smoke-plugin.sh` (gate 3b). README +documents the plugin path (option A) alongside the install script (option B), +explicitly noting the skills+hooks-only scope. + +### Consequences + +**Positive:** +- One source of truth; Codex artifacts can't silently drift from the Claude + manifest. +- Codex users get a one-command marketplace install for skills + hooks. + +**Negative / costs:** +- A third manifest to regenerate (automated; `--check` catches a forgotten + run). +- `policy.authentication` for an app-less plugin is assumed absent pending a + real Codex test-install (tracked as an AIDEV-NOTE in the generator). + +## Alternatives considered + +- See "Considered Options" above (options 2 and 3, both rejected). + +## References + +- `docs/superpowers/specs/2026-06-03-codex-plugin-distribution-design.md` — the + design this ADR records. +- and + — the Codex plugin spec. +- [ADR 0002](0002-agents-md-canonical-claude-md-import.md) — AGENTS.md as the + cross-tool canonical surface (the native Codex path). diff --git a/docs/adr/README.md b/docs/adr/README.md index 1a5019d..5c54f93 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -35,6 +35,7 @@ the original choice. - [0008 — Smart context surfacing via PreToolUse hook (cuts per-session token tax)](0008-smart-context-surfacing-via-pretooluse-hook.md) - [0009 — AGENTS.md lean budget (8 KiB target / 32 KiB hard cap) and stack-check vs repo-doctor severity split](0009-agents-md-lean-budget-and-size-tiers.md) - [0010 — Naming: detect-and-conform over a prescribed house style](0010-naming-detect-and-conform-over-house-style.md) +- [0011 — Codex plugin marketplace as a third distribution path](0011-codex-plugin-marketplace-third-distribution-path.md) (Keep this index in sync with the files in this directory; `/adr-new` will append to it automatically.) diff --git a/docs/maintaining.md b/docs/maintaining.md index 2b4a38b..4a2bc8b 100644 --- a/docs/maintaining.md +++ b/docs/maintaining.md @@ -24,9 +24,17 @@ When you change version or hook configuration: 1. Bump `version` in BOTH `.claude-plugin/plugin.json` and `.claude-plugin/marketplace.json`. They must match — CI fails on drift via `scripts/check_version_sync.py`. -2. Hook scripts use `${CLAUDE_PLUGIN_ROOT}/scripts/hooks/...` in `hooks/hooks.json`. - Never `~` or `$HOME`. -3. `.codex/hooks.json` is a template using `__CLAUDE_LEVERAGE_DIR__` placeholder. +2. Regenerate the Codex plugin artifacts from the Claude source: + ```bash + python scripts/gen-codex-plugin.py + ``` + This rewrites `.codex-plugin/plugin.json` and `.agents/plugins/marketplace.json`. + CI (`codex-plugin-parity`) and `smoke-plugin.sh` fail if they drift. Never + hand-edit the generated files — change `.claude-plugin/` and regenerate. +3. Hook scripts use `${CLAUDE_PLUGIN_ROOT}/scripts/hooks/...` in `hooks/hooks.json`. + Never `~` or `$HOME`. Codex sets `CLAUDE_PLUGIN_ROOT` for compatibility, so + the same file works in both tools. +4. `.codex/hooks.json` is a template using `__CLAUDE_LEVERAGE_DIR__` placeholder. `scripts/install-codex.sh` resolves it at install time when writing to `~/.codex/hooks.json`. @@ -48,6 +56,7 @@ pytest tests/ -v # plugin integrity + frontmatter tests python scripts/check_version_sync.py # plugin.json == marketplace.json shellcheck scripts/hooks/*.sh # CI runs this; install locally to match python scripts/gen-codex-agents.py --check # ensure .codex/agents/*.toml matches agents/ +python scripts/gen-codex-plugin.py --check # ensure .codex-plugin/ + .agents/ match Claude manifest bash scripts/smoke-plugin.sh # single-shot pre-push: all of the above + install-codex e2e ``` diff --git a/docs/superpowers/specs/2026-06-03-codex-plugin-distribution-design.md b/docs/superpowers/specs/2026-06-03-codex-plugin-distribution-design.md new file mode 100644 index 0000000..f42f17c --- /dev/null +++ b/docs/superpowers/specs/2026-06-03-codex-plugin-distribution-design.md @@ -0,0 +1,133 @@ +# Codex plugin distribution for `claude-leverage` + +**Date:** 2026-06-03 +**Status:** Approved (design) — ready for implementation plan + +## Context + +OpenAI Codex recently shipped a plugin system (`/plugins` in Codex CLI, plugin +marketplaces installable from GitHub shorthand, Git URL, SSH URL, or a local +marketplace root). The Codex plugin format is intentionally close to Claude +Code's: + +- Plugin manifest at `.codex-plugin/plugin.json` (vs Claude's `.claude-plugin/plugin.json`). +- Marketplace root at `.agents/plugins/marketplace.json`, with **explicit legacy + support** for `$REPO_ROOT/.claude-plugin/marketplace.json`. +- Skills as `skills//SKILL.md` — identical to Claude. +- Hooks as `hooks/hooks.json`. Codex passes `PLUGIN_ROOT`/`PLUGIN_DATA` **and + also sets `CLAUDE_PLUGIN_ROOT`/`CLAUDE_PLUGIN_DATA` for compatibility**. +- MCP via `.mcp.json`, apps via `.app.json`. + +Sources: , +. + +This repo already integrates with Codex two ways — Codex reads `AGENTS.md` +natively, and `scripts/gen-codex-agents.py` generates `.codex/agents/*.toml` for +subagent parity. **A Codex plugin marketplace is a third, complementary +distribution path**, not a replacement for either. + +### Compatibility audit (current repo vs Codex plugin spec) + +| Component | Status for Codex plugin install | +|---|---| +| Hooks (`${CLAUDE_PLUGIN_ROOT}` in `hooks/hooks.json`) | ✅ works — Codex sets `CLAUDE_PLUGIN_ROOT` for compat | +| Skills (`skills/*/SKILL.md`) | ✅ identical format | +| `plugin.json` | ❌ Codex reads **only** `.codex-plugin/plugin.json`; **no** legacy fallback for the manifest → must add | +| `marketplace.json` | ⚠️ legacy `.claude-plugin/marketplace.json` supported, but schema differs (`interface.displayName` not `owner`; per-plugin `policy` + `category` + `source`) | +| `commands/` (1×), `agents/` (2×) | ❌ not loaded by Codex plugin spec — agents already covered by native `.codex/agents/*.toml` | + +## Decision + +Add a Codex plugin distribution path via **generated, tool-native artifacts** +kept in sync from the existing Claude manifest, plus README/ADR/maintaining +documentation. + +### 1. Source of truth & generated artifacts + +`.claude-plugin/plugin.json` remains the **single source of truth** for +version/metadata. A new generator derives the Codex artifacts from it: + +``` +.claude-plugin/ ← source of truth (Claude reads natively) + plugin.json + marketplace.json +.codex-plugin/ ← GENERATED (Codex reads; no legacy fallback for manifest) + plugin.json +.agents/plugins/ ← GENERATED (canonical Codex marketplace path) + marketplace.json +scripts/gen-codex-plugin.py ← new; mirrors gen-codex-agents.py, supports --check +``` + +### 2. `.codex-plugin/plugin.json` (generated) + +Minimal — Codex defaults cover the rest (`skills/` → `./skills/`, hooks → +`./hooks/hooks.json`). `name`/`version`/`description` mirror the Claude manifest +1:1. No `interface` block (that is for curated-directory submission only — +YAGNI). + +### 3. `.agents/plugins/marketplace.json` (generated) + +Codex schema: top-level `name` + `interface.displayName` (replacing Claude's +`owner`). Per-plugin: `source: {source: "url", url: "…claude-leverage.git"}` +(plugin lives at repo root), `policy.installation: AVAILABLE`, `category`. + +- **Open detail to confirm during implementation:** `policy.authentication` + value when there is no app integration. The plugin ships no MCP/apps, so + expect `NONE` or field omitted — verify via a real test-install in Codex CLI. + +### 4. Documented limitation (not worked around) + +Codex plugin spec loads only **skills + hooks + mcp + apps**. So `/flaky-test` +(1 command) and the 2 subagents do **not** load via the plugin. This is +acceptable: subagents are already delivered through the native +`.codex/agents/*.toml` path + `AGENTS.md`. README frames the two paths as +complementary, not a regression. We do **not** try to hack commands/agents into +the plugin. + +### 5. CI / sync + +`gen-codex-plugin.py --check` is added to `scripts/smoke-plugin.sh` next to +`gen-codex-agents.py --check`. Version drift across the three manifests is thus +impossible — all Codex artifacts derive from `.claude-plugin/plugin.json`. + +### 6. README + +New "Install as a Codex plugin" section: add the marketplace via GitHub +shorthand `Filip-Podstavec/claude-leverage`, then `/plugins` → install. Includes +the explicit limitation from §4 and a pointer to the native `.codex/` path for +agents/commands. + +### 7. ADR + maintaining.md + +- ADR (via `/adr-new`): "Codex plugin marketplace as a third distribution path" + — context (Codex shipped plugins; format ≈ Claude Code clone), decision + (generated tool-native artifacts), alternatives rejected (augmented shared + `.claude-plugin/marketplace.json`; hand-maintained files + version-sync guard). +- `docs/maintaining.md`: add a step — "when you change version/skills/hooks, run + `gen-codex-plugin.py`" — beside the existing `gen-codex-agents.py` step. + +## Alternatives considered (rejected) + +- **Augment the shared `.claude-plugin/marketplace.json`** with Codex fields + (Codex reads it via the legacy path). Fewer files, but mixes two schemas in + one file and still requires a hand-written `.codex-plugin/plugin.json`. +- **Hand-write both Codex files + extend `check_version_sync.py`** to assert + version equality. No new generator, but more manual upkeep on every change and + weaker than generation at preventing structural drift. + +Both rejected in favor of generation, which matches the repo's existing +generated-and-`--check`ed-artifact idiom (`gen-codex-agents.py`). + +## Out of scope + +- Submitting to OpenAI's curated plugin directory (would need the `interface` + block, assets, policy/ToS URLs). +- Any change to the existing native `.codex/` install path. +- MCP/apps support (the plugin ships none). + +## Open questions + +1. `policy.authentication` enum value for an app-less plugin — confirm via + test-install (§3). +2. Branch: this work is unrelated to the current `feat/adherence-scorer` branch; + implementation should start on a fresh branch off `main`. diff --git a/scripts/gen-codex-plugin.py b/scripts/gen-codex-plugin.py new file mode 100644 index 0000000..a9072a8 --- /dev/null +++ b/scripts/gen-codex-plugin.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +"""Generate the Codex plugin artifacts from the Claude manifest pair. + +Codex shipped a plugin marketplace whose format is close to Claude Code's, but +the plugin manifest has no legacy fallback (Codex reads ONLY +`.codex-plugin/plugin.json`), and the marketplace schema differs +(`interface.displayName` instead of `owner`, per-plugin `policy`). Rather than +hand-maintain a second + third manifest, we derive them from the Claude source +of truth: + + .claude-plugin/plugin.json -> .codex-plugin/plugin.json + .claude-plugin/marketplace.json -> .agents/plugins/marketplace.json + +Run without args to regenerate: + python scripts/gen-codex-plugin.py + +Run with --check to fail (exit 1) if regeneration would change any output file. +CI uses this to enforce parity. --dry-run prints the diff without writing. + +Stdlib-only, matching scripts/gen-codex-agents.py. +""" +from __future__ import annotations + +import argparse +import difflib +import json +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +CLAUDE_PLUGIN = REPO_ROOT / ".claude-plugin" / "plugin.json" +CLAUDE_MARKETPLACE = REPO_ROOT / ".claude-plugin" / "marketplace.json" +CODEX_PLUGIN = REPO_ROOT / ".codex-plugin" / "plugin.json" +CODEX_MARKETPLACE = REPO_ROOT / ".agents" / "plugins" / "marketplace.json" +PLUGIN_NAME = "claude-leverage" + +# `skills` and `hooks` are documented optional manifest fields (see +# developers.openai.com/codex/plugins/build). Codex defaults already resolve +# skills/ and hooks/hooks.json, but we point at them explicitly so the security +# hooks load deterministically regardless of any future default change. +SKILLS_POINTER = "./skills/" +HOOKS_POINTER = "./hooks/hooks.json" + + +def build_codex_plugin(plugin_src: dict) -> dict: + """Map .claude-plugin/plugin.json -> .codex-plugin/plugin.json. + + Carries the descriptive metadata verbatim and adds explicit component + pointers. Key order here IS the on-disk order (json.dumps preserves it), + so --check stays stable.""" + out: dict = { + "name": plugin_src["name"], + "version": plugin_src["version"], + "description": plugin_src["description"], + } + for key in ("author", "homepage", "repository", "license", "keywords"): + if key in plugin_src: + out[key] = plugin_src[key] + out["skills"] = SKILLS_POINTER + out["hooks"] = HOOKS_POINTER + return out + + +def build_codex_marketplace(marketplace_src: dict) -> dict: + """Map .claude-plugin/marketplace.json -> .agents/plugins/marketplace.json. + + Claude's per-plugin `source: {source: "url", url: ...}` is already Codex's + "url" source shape, so it copies through. The transform is: owner -> + interface.displayName, and add per-plugin policy.""" + out: dict = {"name": marketplace_src["name"]} + owner = marketplace_src.get("owner", {}) + if owner.get("name"): + out["interface"] = {"displayName": owner["name"]} + + plugins_out = [] + for entry in marketplace_src.get("plugins", []): + p: dict = { + "name": entry["name"], + "version": entry["version"], + "description": entry["description"], + "source": entry["source"], + # AIDEV-NOTE: no `policy.authentication` — this plugin ships no app + # integration. Add it (e.g. "ON_INSTALL") only if a Codex + # test-install proves it's required for an app-less plugin. + "policy": {"installation": "AVAILABLE"}, + } + for key in ("category", "keywords"): + if key in entry: + p[key] = entry[key] + plugins_out.append(p) + out["plugins"] = plugins_out + return out + + +def render(obj: dict) -> str: + """Pretty JSON with a trailing newline (matches the Claude manifests).""" + return json.dumps(obj, indent=2, ensure_ascii=False) + "\n" + + +def _targets() -> list[tuple[Path, str]]: + plugin_src = json.loads(CLAUDE_PLUGIN.read_text(encoding="utf-8")) + marketplace_src = json.loads(CLAUDE_MARKETPLACE.read_text(encoding="utf-8")) + return [ + (CODEX_PLUGIN, render(build_codex_plugin(plugin_src))), + (CODEX_MARKETPLACE, render(build_codex_marketplace(marketplace_src))), + ] + + +def main() -> int: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--check", action="store_true", + help="Exit 1 if any generated file differs from disk. No writes.") + p.add_argument("--dry-run", action="store_true", + help="Print the diff vs disk without writing.") + args = p.parse_args() + + if args.check and args.dry_run: + print("ERROR: --check and --dry-run are mutually exclusive", file=sys.stderr) + return 2 + + for src in (CLAUDE_PLUGIN, CLAUDE_MARKETPLACE): + if not src.is_file(): + print(f"missing source {src.relative_to(REPO_ROOT)}", file=sys.stderr) + return 1 + + drift = 0 + written = 0 + for out_path, generated in _targets(): + rel = out_path.relative_to(REPO_ROOT) + existing = out_path.read_text(encoding="utf-8") if out_path.is_file() else "" + + if existing == generated: + print(f"OK {rel} (no change)") + continue + + if args.check or args.dry_run: + drift += 1 + label = "DRIFT" if args.check else "WOULD WRITE" + stream = sys.stderr if args.check else sys.stdout + print(f"{label} {rel}", file=stream) + for line in difflib.unified_diff( + existing.splitlines(keepends=True), + generated.splitlines(keepends=True), + fromfile=str(rel) + " (on disk)", + tofile=str(rel) + " (generated)", + n=2, + ): + stream.write(line) + continue + + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(generated, encoding="utf-8") + written += 1 + print(f"WRITE {rel}") + + if args.check: + if drift: + print(f"\n{drift} file(s) drifted. Re-run without --check to regenerate.", + file=sys.stderr) + return 1 + print("all Codex plugin artifacts match the Claude manifest source") + return 0 + if args.dry_run: + print(f"\n{drift} file(s) would change." if drift else "\nno changes.") + return 0 + print(f"\ngenerated {written} file(s).") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/hooks/ai-first-nudge.sh b/scripts/hooks/ai-first-nudge.sh index 3bb17fc..5ace6f8 100755 --- a/scripts/hooks/ai-first-nudge.sh +++ b/scripts/hooks/ai-first-nudge.sh @@ -71,7 +71,7 @@ file_path=$(get_field '.tool_input.file_path' 2>/dev/null) || true # Applied to BOTH nudges — these paths are noise for both checks. # # AIDEV-NOTE: split into basename-only vs path patterns so a directory -# named e.g. slevomat_test_api/ does not silently swallow nudges for +# named e.g. acme_test_api/ does not silently swallow nudges for # production code inside it (v1.4.0 field-feedback #1). Also normalize # Windows backslashes before path matching so paths Claude Code sends # in C:\…\node_modules\… form still hit the ignore list (#2). diff --git a/scripts/install-codex.sh b/scripts/install-codex.sh index 6bddc8c..d28ed66 100755 --- a/scripts/install-codex.sh +++ b/scripts/install-codex.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash # install-codex.sh — Install the claude-leverage stack into Codex CLI. # -# Codex has no plugin marketplace, so this script is the equivalent of -# `/plugin install` for Codex: +# Codex's plugin marketplace delivers skills + hooks, but not subagents, the +# global @AGENTS.md import, or slash commands. This script is the full-stack +# install for Codex — the equivalent of `/plugin install` plus those extras: # 1. Resolves __CLAUDE_LEVERAGE_DIR__ in .codex/hooks.json to this repo's # absolute path and writes the resolved file to ~/.codex/hooks.json # (creating a backup of any existing file). diff --git a/scripts/smoke-plugin.sh b/scripts/smoke-plugin.sh index d6bee02..635faca 100755 --- a/scripts/smoke-plugin.sh +++ b/scripts/smoke-plugin.sh @@ -69,6 +69,18 @@ else failed=$((failed + 1)) fi +# ---------------------------------------------------------------------- +# 3b. codex plugin artifacts parity +# ---------------------------------------------------------------------- +say "3b. .codex-plugin/ + .agents/plugins/ match Claude manifest" +if python scripts/gen-codex-plugin.py --check >$SCRATCH/codexplugin.log 2>&1; then + say_pass "Codex plugin artifacts in sync" +else + say_fail "codex plugin artifacts drifted — run: python scripts/gen-codex-plugin.py" + cat $SCRATCH/codexplugin.log >&2 + failed=$((failed + 1)) +fi + # ---------------------------------------------------------------------- # 4. context-map manifest is up to date # ---------------------------------------------------------------------- diff --git a/tests/test_codex_plugin_sync.py b/tests/test_codex_plugin_sync.py new file mode 100644 index 0000000..63db20e --- /dev/null +++ b/tests/test_codex_plugin_sync.py @@ -0,0 +1,58 @@ +"""The generated Codex plugin artifacts must stay in sync with the Claude +manifest pair that is their single source of truth. + +Mirrors the contract that CI enforces with `gen-codex-plugin.py --check`, but +as importable unit tests so a drift shows up in the normal pytest run too. +""" +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +GEN_PATH = REPO_ROOT / "scripts" / "gen-codex-plugin.py" +CODEX_PLUGIN = REPO_ROOT / ".codex-plugin" / "plugin.json" +CODEX_MARKETPLACE = REPO_ROOT / ".agents" / "plugins" / "marketplace.json" +CLAUDE_PLUGIN = REPO_ROOT / ".claude-plugin" / "plugin.json" +CLAUDE_MARKETPLACE = REPO_ROOT / ".claude-plugin" / "marketplace.json" +PLUGIN_NAME = "claude-leverage" + + +def _load_generator(): + spec = importlib.util.spec_from_file_location("gen_codex_plugin", GEN_PATH) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _read_json(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def test_codex_plugin_manifest_matches_generator(): + gen = _load_generator() + plugin_src = _read_json(CLAUDE_PLUGIN) + expected = gen.build_codex_plugin(plugin_src) + assert _read_json(CODEX_PLUGIN) == expected, ( + "Run: python scripts/gen-codex-plugin.py" + ) + + +def test_codex_marketplace_matches_generator(): + gen = _load_generator() + marketplace_src = _read_json(CLAUDE_MARKETPLACE) + expected = gen.build_codex_marketplace(marketplace_src) + assert _read_json(CODEX_MARKETPLACE) == expected, ( + "Run: python scripts/gen-codex-plugin.py" + ) + + +def test_versions_agree_across_all_manifests(): + claude_v = _read_json(CLAUDE_PLUGIN)["version"] + codex_v = _read_json(CODEX_PLUGIN)["version"] + entry = next( + p for p in _read_json(CODEX_MARKETPLACE)["plugins"] + if p["name"] == PLUGIN_NAME + ) + assert claude_v == codex_v == entry["version"] diff --git a/tests/test_hook_behavior.py b/tests/test_hook_behavior.py index fccb3c2..83bb7c6 100644 --- a/tests/test_hook_behavior.py +++ b/tests/test_hook_behavior.py @@ -89,10 +89,10 @@ def _ai_first_nudge_payload(file_path: str, *, loc: int = 60) -> dict: def test_ai_first_nudge_fires_when_dir_name_contains_test(tmp_path: Path) -> None: - """A directory called slevomat_test_api/ must NOT make the hook treat + """A directory called acme_test_api/ must NOT make the hook treat files inside it as test files. Only basename test_*.py / *_test.py should be ignored as tests. Regression for v1.4.0 field-feedback #1.""" - project = tmp_path / "slevomat_test_api" + project = tmp_path / "acme_test_api" project.mkdir() target = project / "discover.py" payload = _ai_first_nudge_payload(str(target))