diff --git a/.github/release-please-config.json b/.github/release-please-config.json index d5f6160e6..fa915325c 100644 --- a/.github/release-please-config.json +++ b/.github/release-please-config.json @@ -47,7 +47,8 @@ ".kimi-plugin/marketplace.json", ".claude-plugin/marketplace.json", ".cursor-plugin/marketplace.json", - ".grok-plugin/marketplace.json" + ".grok-plugin/marketplace.json", + ".omp-plugin/marketplace.json" ], "extra-files": [ { @@ -85,6 +86,11 @@ "path": ".devin-plugin/plugin.json", "jsonpath": "$.version" }, + { + "type": "json", + "path": ".omp-plugin/marketplace.json", + "jsonpath": "$.plugins[0].version" + }, { "type": "json", "path": "plugin.json", diff --git a/.omp-plugin/marketplace.json b/.omp-plugin/marketplace.json new file mode 100644 index 000000000..6b21bd9f5 --- /dev/null +++ b/.omp-plugin/marketplace.json @@ -0,0 +1,29 @@ +{ + "name": "compound-engineering-plugin", + "owner": { + "name": "Kieran Klaassen and Trevin Chow" + }, + "metadata": { + "description": "Plugin marketplace for Claude Code and Codex extensions" + }, + "plugins": [ + { + "name": "compound-engineering", + "description": "Brainstorm, plan, debug, review, and compound learnings with AI agents", + "version": "3.21.4", + "author": { + "name": "Kieran Klaassen and Trevin Chow" + }, + "homepage": "https://github.com/EveryInc/compound-engineering-plugin", + "tags": [ + "ai-powered", + "compound-engineering", + "workflow-automation", + "code-review", + "quality", + "knowledge-management" + ], + "source": "./" + } + ] +} diff --git a/AGENTS.md b/AGENTS.md index 3d26e9423..f7a488aad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -124,7 +124,7 @@ That field guide is the canonical reasoning layer for outcome-first authoring, m ### User-Facing Skill Invocations -Keep agent-to-agent or skill-to-skill routing semantic: format formal skill names as inline code (for example, `ce-plan`) and invoke the named skill through the active harness's callable skill mechanism. When a skill prints or copies a user-runnable invocation, default to `/skill-name`; use `$skill-name` only when the active harness is Codex or explicitly documents dollar-prefixed skill invocation. In prose, render only the invocation as inline code; use a fenced block only when the command stands alone. Output exactly one form. Do not apply this rendering rule to built-in commands such as `/goal`. +Keep agent-to-agent or skill-to-skill routing semantic: format formal skill names as inline code (for example, `ce-plan`) and invoke the named skill through the active harness's callable skill mechanism. When a skill prints or copies a user-runnable invocation, default to `/skill-name`; use `$skill-name` only when the active harness is Codex or explicitly documents dollar-prefixed skill invocation. On oh-my-pi (`omp`), keep the default form for model-visible targets; use native `/skill:` only when the target is not model-visible because it declares `disable-model-invocation` or `hide` (for example, `/skill:ce-polish`). In prose, render only the invocation as inline code; use a fenced block only when the command stands alone. Output exactly one form. Do not apply this rendering rule to built-in commands such as `/goal`. At runtime, put the smallest self-contained rendering rule immediately before the smallest section that contains all affected user-copy seams. Do not repeat it in every step; repeat it only in a separately loaded reference that independently owns output. diff --git a/README.md b/README.md index aeecc6a1c..e1a9f6e8c 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Remove the obsolete Compound Engineering Codex tool-map block from my Codex home Re-running the Bun convert/install CLI for Codex also strips the block if it is still present; it no longer inserts it. -**Another editor or CLI?** Kimi Code CLI, Cline, Grok Build CLI, Devin CLI, GitHub Copilot, Factory Droid, Qwen Code, OpenCode, Pi, and Antigravity CLI are all supported — see [More install options](#more-install-options). +**Another editor or CLI?** Kimi Code CLI, Cline, Grok Build CLI, Devin CLI, GitHub Copilot, Factory Droid, Qwen Code, OpenCode, Pi, oh-my-pi (omp), and Antigravity CLI are all supported — see [More install options](#more-install-options). --- @@ -103,7 +103,7 @@ Re-running the Bun convert/install CLI for Codex also strips the block if it is **Each unit of engineering work should make subsequent units easier -- not harder.** -Invocation syntax: this README uses `/skill-name` examples for slash-skill hosts. In Codex, invoke installed skills with `$skill-name` (for example, `$ce-plan` and `$lfg`); `/goal` remains a Codex built-in command. +Invocation syntax: this README uses `/skill-name` examples for slash-skill hosts. In Codex, invoke installed skills with `$skill-name` (for example, `$ce-plan` and `$lfg`). In oh-my-pi (omp), these prompts can model-route to visible skills; use the native deterministic `/skill:` form for manual-only or hidden skills (for example, `/skill:ce-polish`). `/goal` remains a Codex built-in command. Traditional development accumulates technical debt. Every feature adds complexity. Every bug fix leaves behind a little more local knowledge that someone has to rediscover later. The codebase gets larger, the context gets harder to hold, and the next change becomes slower. @@ -400,6 +400,38 @@ Recommended companion for richer blocking questions: pi install npm:pi-ask-user ``` +### oh-my-pi (omp) + +oh-my-pi (omp) installs Compound Engineering through its marketplace flow. The repo ships a native `.omp-plugin/marketplace.json` catalog whose plugin entry carries a release-managed `version`, so omp's update checker can see each new CE release: + +```text +omp plugin marketplace add EveryInc/compound-engineering-plugin +omp plugin install compound-engineering@compound-engineering-plugin +``` + +To stay current automatically, enable auto-update: + +```bash +omp config set marketplace.autoUpdate auto +``` + +The default `notify` mode only writes update availability to the debug log — it does not prompt — so without `auto` you will not hear about new releases. To upgrade by hand instead, run `omp plugin upgrade compound-engineering@compound-engineering-plugin`. + +
+Other install paths (pin-style and contributor development) + +`omp install https://github.com/EveryInc/compound-engineering-plugin` installs the repository as an npm-style plugin. That path has **no update mechanism** — treat it as pinning a snapshot, not as the recommended install. + +For local development from a checkout, use a live symlink instead: + +```bash +omp plugin link "$PWD" +``` + +
+ +Run `/reload-plugins` or start a new omp session after installing so the skills load. omp's native deterministic command is `/skill:` (for example, `/skill:ce-plan`); ordinary `/skill-name` prompts can also model-route to visible skills, but manual-only or hidden skills require the native form. See [`docs/specs/omp.md`](docs/specs/omp.md) for details. + ### Antigravity CLI (`agy`) Google has replaced the consumer Gemini CLI with [Antigravity CLI](https://antigravity.google) (`agy`), which still runs on Gemini models. Install Compound Engineering directly from GitHub — no clone step required: @@ -565,6 +597,12 @@ Restart OpenCode after changing `opencode.json`. pi -e "$PWD" ``` +**oh-my-pi (omp)** + +```bash +omp plugin link "$PWD" +``` + **Antigravity CLI (`agy`)** ```bash @@ -582,7 +620,7 @@ See [`.agy/INSTALL.md`](.agy/INSTALL.md) for remote install and pinning examples ## Limitations -OpenCode and Pi use native package/plugin loading from this repository. The Bun CLI remains for repository development and converter maintenance, not normal installation. +OpenCode, Pi, and oh-my-pi (omp) use native package/plugin loading from this repository. The Bun CLI remains for repository development and converter maintenance, not normal installation. Release versions are owned by release automation. Routine feature PRs should not hand-bump plugin or marketplace manifest versions. diff --git a/docs/solutions/skill-design/portable-agent-skill-authoring.md b/docs/solutions/skill-design/portable-agent-skill-authoring.md index 5b46e8b8f..4b89b9875 100644 --- a/docs/solutions/skill-design/portable-agent-skill-authoring.md +++ b/docs/solutions/skill-design/portable-agent-skill-authoring.md @@ -158,7 +158,7 @@ Evaluate activation separately from execution with a few positive triggers, adja ### Render user invocations at the output boundary -Keep agent-to-agent routing capability-first: format formal skill names as inline code (for example, `ce-plan`) and invoke the named skill through the active harness's callable skill mechanism. Exact command spelling belongs only where the skill prints or copies a user-runnable invocation. At that output seam, default to `/skill-name`; use `$skill-name` only when the active harness is Codex or explicitly documents dollar-prefixed skill invocation. In prose, render only the invocation as inline code; use a fenced block only when the command stands alone. Output exactly one form. Built-in commands such as `/goal` are separate capabilities, not evidence that slash-prefixed skill names are callable in Codex. +Keep agent-to-agent routing capability-first: format formal skill names as inline code (for example, `ce-plan`) and invoke the named skill through the active harness's callable skill mechanism. Exact command spelling belongs only where the skill prints or copies a user-runnable invocation. At that output seam, default to `/skill-name`; use `$skill-name` only when the active harness is Codex or explicitly documents dollar-prefixed skill invocation. On oh-my-pi (`omp`), keep the default form for model-visible targets; use native `/skill:` only when the target is not model-visible because it declares `disable-model-invocation` or `hide` (for example, `/skill:ce-polish`). In prose, render only the invocation as inline code; use a fenced block only when the command stands alone. Output exactly one form. Built-in commands such as `/goal` are separate capabilities, not evidence that slash-prefixed skill names are callable in Codex. An authoring guide cannot supply runtime behavior to an installed skill. Put the smallest self-contained rendering rule immediately before the smallest section that contains all affected user-copy seams. Do not repeat it in every step; repeat it only in a separately loaded reference that independently owns output. Use a focused contract test when independently edited skills must preserve the same handoff, without duplicating the rationale or a harness matrix. diff --git a/docs/specs/omp.md b/docs/specs/omp.md new file mode 100644 index 000000000..409b706fb --- /dev/null +++ b/docs/specs/omp.md @@ -0,0 +1,111 @@ +# oh-my-pi (omp) Spec (Plugins and Skills) + +Last verified: 2026-08-05 against omp 17.2.9 + +## Primary sources + +``` +https://github.com/can1357/oh-my-pi/blob/v17.2.9/README.md +https://github.com/can1357/oh-my-pi/blob/v17.2.9/docs/skills.md +https://github.com/can1357/oh-my-pi/blob/v17.2.9/docs/marketplace.md +https://github.com/can1357/oh-my-pi/blob/v17.2.9/docs/session.md +https://github.com/can1357/oh-my-pi/blob/v17.2.9/docs/config-usage.md +https://github.com/can1357/oh-my-pi/blob/v17.2.9/docs/environment-variables.md +https://github.com/can1357/oh-my-pi/blob/v17.2.9/docs/task-agent-discovery.md +``` + +## Plugin loading + +omp discovers plugins natively. Two committed metadata surfaces in this repository cover it: + +- The `package.json#pi` manifest — the same pi package metadata Pi already consumes. omp's shared plugin loader accepts `package.json.omp || package.json.pi`, so the existing `pi` field is the discovery gate that marks this repository as a plugin package. +- The native omp marketplace catalog at `.omp-plugin/marketplace.json`, which omp reads first. `.claude-plugin/marketplace.json` remains as the Claude Code copy and as omp's fallback when `.omp-plugin/marketplace.json` is absent; the two coexist, and omp ignores the Claude copy when both are present (verified 2026-08-05: discovery output and the cached catalog both come from the `.omp-plugin` copy). + +Skill loading is structural, not extension-driven: omp's skill providers scan the plugin package's root `skills/` directory (`omp-plugins` provider for npm/link installs, `claude-plugins` provider for marketplace installs). The bundled extension `.pi/extensions/compound-engineering.ts` is a **no-op on omp** — it registers skill paths through the `resources_discover` hook, and omp implements `ExtensionRunner.emitResourcesDiscover(...)` with no `AgentSession` callsites. The extension still matters in one narrow way: install validates that every declared `extensions` entry resolves and imports to a factory function, and rolls the install back if it does not. So the `pi` block is a discovery gate and an install-time validation risk, but it is not what surfaces the skills. + +A dry run of `omp install` against this repository confirms the metadata surfaces resolve. No CE converter, writer, or `--to omp` CLI target exists or is planned: per CONCEPTS.md "Native plugin surface", omp support lives in platform metadata, docs, and release validation instead of a new Converter and Writer. + +## Updates + +omp's update checker (`checkForUpdates()`) compares the installed plugin version against the **catalog plugin-entry `version`** and permanently skips entries that lack one: + +```ts +catalogVersion = catalog.plugins.find(p => p.name === parsed.name)?.version; +if (!catalogVersion || catalogVersion === installed.version) continue; +``` + +`.omp-plugin/marketplace.json` therefore carries a release-managed `version` on the plugin entry, bumped by release-please through the root component's `extra-files` (`$.plugins[0].version`). This is what makes `omp plugin upgrade`, the 24h catalog refresh, and `marketplace.autoUpdate` (`off` / `notify` / `auto`) see CE releases at all. Note the default `notify` mode writes update availability only to the debug log — it shows no user-facing notification — so `omp config set marketplace.autoUpdate auto` is the setting that actually keeps an install current. Verified end to end on 17.2.9: with the catalog version bumped, `omp plugin upgrade` reinstalls into a new version-keyed cache directory and repoints the `node_modules` symlink; without it, the same change reports "up to date". + +Only the marketplace install path has an update story. `omp install ` (npm-style plugin install) has none — treat it as pinning a snapshot. + +## Install commands + +Marketplace flow — the recommended install (marketplace name `compound-engineering-plugin`, plugin name `compound-engineering`, both from `.omp-plugin/marketplace.json`): + +```text +omp plugin marketplace add EveryInc/compound-engineering-plugin +omp plugin install compound-engineering@compound-engineering-plugin +``` + +Stay current: + +```bash +omp config set marketplace.autoUpdate auto # or: omp plugin upgrade compound-engineering@compound-engineering-plugin +``` + +Pin-style direct install from a path or Git URL (no update mechanism; user scope by default): + +```text +omp install https://github.com/EveryInc/compound-engineering-plugin +``` + +Local development link from a checkout: + +```bash +omp plugin link "$PWD" +``` + +Verify an install plan before applying it: + +```text +omp install --dry-run --json +``` + +`/reload-plugins` refreshes skills and slash commands in a live session; restart omp for tools, hooks, or extension changes to apply. + +## Runtime contracts CE skills rely on + +| Contract | omp behavior | +| --- | --- | +| Model-routed skill prompt | An otherwise unknown `/skill-name ...` prompt reaches the model, which can select a visible discovered skill; this is convenient, not deterministic | +| Native deterministic skill invocation | `/skill:` — one registered command per discovered skill; use this for manual-only or hidden skills that are not model-visible | +| Blocking questions | Built-in `ask` tool | +| Subagent dispatch | Built-in `task` tool, with worktree isolation and schema-checked results | +| Task tracking | Built-in `todo` tool | +| MCP | Native MCP server support | +| Bundled skill files | `skill:///` URL resolution | + +## Instruction files + +omp auto-loads `AGENTS.md`, walking ancestors from the current working directory. This repo's root `AGENTS.md` is already the canonical project instruction file for omp, so no CE action is needed. + +## Session storage + +omp writes sessions as JSONL under a session root resolved in this order: + +1. `$PI_CODING_AGENT_SESSION_DIR` — direct override; files are stored flat in it. +2. `$PI_CODING_AGENT_DIR` — agent-dir override, honored for the default profile only; sessions land in `/sessions/`. +3. `$HOME/${PI_CONFIG_DIR:-.omp}/agent/sessions/` — default location. + +Named profiles (`OMP_PROFILE` or `PI_PROFILE`) relocate the root to `$HOME/${PI_CONFIG_DIR:-.omp}/profiles//agent/sessions/`. + +Inside the session root, per-project buckets come in two shapes. omp 17.2.9 restored the legacy project-scoped naming scheme and removed its automatic migration ([#7646](https://github.com/can1357/oh-my-pi/issues/7646)), so both shapes occur in the wild and discovery must scan both: + +- Raw (current again since 17.2.9): `-` for cwds under the canonical home, `-tmp-` for cwds under the temp root, and `----` otherwise, with path separators and `:` encoded as `-` and the basename kept verbatim (spaces included). +- Hashed (intermediate releases): `--`, where `scope` is `home`, `tmp`, or `abs` and the basename is sanitized (`[^a-zA-Z0-9._-]+` runs become `-`, edge dashes stripped, capped at its last 80 chars, empty falls back to `project`). + +Each bucket holds `_.jsonl` files. + +Every session JSONL physically begins with a fixed-width 256-byte `{"type":"title","v":1,...,"pad":"..."}` slot line, followed by a pi-shaped `{"type":"session","version":3,...,"cwd":...}` header. This title-slot-first shape distinguishes omp session files from pi session files, which start directly with the `type:"session"` header. + +Known gap: CE's session-history discovery does not yet support omp. The entire integration — including default, override, named-profile, and XDG-relocated roots — is deferred to [#1333](https://github.com/EveryInc/compound-engineering-plugin/pull/1333). diff --git a/skills/ce-dogfood/SKILL.md b/skills/ce-dogfood/SKILL.md index 3b0275b30..1b622c2ed 100644 --- a/skills/ce-dogfood/SKILL.md +++ b/skills/ce-dogfood/SKILL.md @@ -17,7 +17,7 @@ This workflow drives the browser exclusively through the `agent-browser` CLI. Do ## Prerequisites -**User-runnable invocation rendering.** In prerequisite failures, default to `/ce-setup` and `/ce-dogfood `; use `$ce-setup` and `$ce-dogfood ` only when the active host is Codex or explicitly documents dollar-prefixed skill invocation. Render only each invocation as inline code and output one form only. +**User-runnable invocation rendering.** In prerequisite failures, default to `/ce-setup` and `/ce-dogfood `; use `$ce-setup` and `$ce-dogfood ` only when the active host is Codex or explicitly documents dollar-prefixed skill invocation. On oh-my-pi (`omp`), use `/skill:ce-setup` and `/skill:ce-dogfood `. Render only each invocation as inline code and output one form only. - A local dev server you can start (`bin/dev`, `rails server`, `npm run dev`, etc.). - `agent-browser` installed. Check: diff --git a/skills/ce-explain/SKILL.md b/skills/ce-explain/SKILL.md index aa4047bbf..54cab3e45 100644 --- a/skills/ce-explain/SKILL.md +++ b/skills/ce-explain/SKILL.md @@ -124,7 +124,7 @@ Detect destinations by capability — probe the agent's own toolset and session **Improvement observations.** When composing the explainer surfaced things that could be better, route them by type once the destination is settled — offer, don't auto-fire. "Settled" means the artifact was sent, or the user declined, or the run stopped at a consent gate they didn't answer; in that last case the run ends there and these offers are skipped, like the non-interactive case above. Never raise them while any of the asks above is still open — the destination question, the audience re-render offer, or a publisher's consent gate. -**User-runnable invocation rendering.** Only the user-run handoff below uses printed invocation syntax. Default to `/ce-polish`; use `$ce-polish` only when the active host is Codex or explicitly documents dollar-prefixed skill invocation. Render only the invocation as inline code and output one form only. +**User-runnable invocation rendering.** Only the user-run handoff below uses printed invocation syntax. Default to `/ce-polish`; use `$ce-polish` only when the active host is Codex or explicitly documents dollar-prefixed skill invocation. On oh-my-pi (`omp`), use `/skill:ce-polish`. Render only the invocation as inline code and output one form only. - **New-capability ideas** — offer first; on acceptance invoke the `ce-ideate` skill via the platform's skill-invocation primitive, passing the observations as seed context. Do not merely tell the user to run it. - **Code-clarity findings** — offer first; on acceptance invoke the `ce-simplify-code` skill via the platform's skill-invocation primitive, passing the observations and the files they concern. Do not merely tell the user to run it. diff --git a/skills/ce-optimize/references/usage-guide.md b/skills/ce-optimize/references/usage-guide.md index 7eb607578..1947479f8 100644 --- a/skills/ce-optimize/references/usage-guide.md +++ b/skills/ce-optimize/references/usage-guide.md @@ -79,7 +79,7 @@ The goal of the first run is to validate the harness, not to win the optimizatio ### 1. Memory Tuning ```text -Use /ce-optimize to find the smallest memory setting that keeps this service stable under our load test. +Run the `ce-optimize` skill to find the smallest memory setting that keeps this service stable under our load test. The current container limit is 512 MB and the app sometimes OOM-crashes. Do not just jump to 8 GB. Try a small set of realistic memory limits, run the same load test for each one, and score the results using: - did the process OOM @@ -92,7 +92,7 @@ Prefer the smallest memory limit that passes the guard rails. ### 2. Clustering Quality ```text -Use /ce-optimize to improve issue and PR clustering quality. +Run the `ce-optimize` skill to improve issue and PR clustering quality. We have about 18k open issues and PRs. We want to test changes that improve clustering quality, reduce singleton clusters, and improve match quality within each cluster. @@ -104,7 +104,7 @@ Do not optimize only for coverage. Use LLM-as-judge to sample clusters and confi ### 3. Prompt Optimization ```text -Use /ce-optimize to create a summarization prompt for issues and PRs that minimizes token spend while still producing summaries that are good enough for downstream clustering. +Run the `ce-optimize` skill to create a summarization prompt for issues and PRs that minimizes token spend while still producing summaries that are good enough for downstream clustering. I want the loop to compare prompt variants, measure token cost, and judge whether the summaries preserve the distinctions needed to cluster related issues together without merging unrelated ones. ``` diff --git a/skills/ce-setup/SKILL.md b/skills/ce-setup/SKILL.md index e43d06afe..9f1bc20a7 100644 --- a/skills/ce-setup/SKILL.md +++ b/skills/ce-setup/SKILL.md @@ -61,7 +61,7 @@ Display the diagnostic output to the user. Missing optional tools are not setup ### Step 3: Decide Whether Fixes Are Needed -**User-runnable invocation rendering.** In setup summaries, default to `/ce-setup`; use `$ce-setup` only when the active host is Codex or explicitly documents dollar-prefixed skill invocation. Render only the invocation as inline code and output one form only. +**User-runnable invocation rendering.** In setup summaries, default to `/ce-setup`; use `$ce-setup` only when the active host is Codex or explicitly documents dollar-prefixed skill invocation. On oh-my-pi (`omp`), use `/skill:ce-setup`. Render only the invocation as inline code and output one form only. Proceed to Phase 2 only if one or more repo-local project issues exist: diff --git a/skills/ce-sweep/references/interview.md b/skills/ce-sweep/references/interview.md index 368a9ef95..b82cf8f56 100644 --- a/skills/ce-sweep/references/interview.md +++ b/skills/ce-sweep/references/interview.md @@ -4,7 +4,7 @@ Loaded by `SKILL.md` when `ce-sweep` runs with no `feedback_sources` configured. This interview is **interactive only**. The caller refuses first-run setup in non-interactive mode — a scheduled or piped run with no config aborts and tells the user to run `ce-sweep` interactively once. Do not attempt to infer sources, actions, or approvals without asking. -**User-runnable invocation rendering.** Whenever this interview prints or registers a `ce-sweep` invocation, default to `/ce-sweep` (plus any arguments); use `$ce-sweep` only when the active host is Codex or explicitly documents dollar-prefixed skill invocation. Render only the invocation as inline code and output one form only. +**User-runnable invocation rendering.** Whenever this interview prints or registers a `ce-sweep` invocation, default to `/ce-sweep` (plus any arguments); use `$ce-sweep` only when the active host is Codex or explicitly documents dollar-prefixed skill invocation. On oh-my-pi (`omp`), use `/skill:ce-sweep` (plus any arguments). Render only the invocation as inline code and output one form only. ## Interaction Method diff --git a/src/commands/convert.ts b/src/commands/convert.ts index 4982d97fa..e55c250a1 100644 --- a/src/commands/convert.ts +++ b/src/commands/convert.ts @@ -98,7 +98,7 @@ export default defineCommand({ const activeTargets = detected.filter((t) => t.detected && targets[t.name]?.implemented) if (activeTargets.length === 0) { - console.log("No installable AI coding tools detected. Use native plugin install for Claude Code, Copilot, Droid, OpenCode, Pi, and Qwen.") + console.log("No installable AI coding tools detected. Use native plugin install for Claude Code, Copilot, Droid, oh-my-pi (omp), OpenCode, Pi, and Qwen.") return } diff --git a/src/commands/install.ts b/src/commands/install.ts index 9ff23e5d5..9809a47ff 100644 --- a/src/commands/install.ts +++ b/src/commands/install.ts @@ -109,7 +109,7 @@ export default defineCommand({ const activeTargets = detected.filter((t) => t.detected && targets[t.name]?.implemented) if (activeTargets.length === 0) { - console.log("No installable AI coding tools detected. Use native plugin install for Claude Code, Copilot, Droid, OpenCode, Pi, and Qwen.") + console.log("No installable AI coding tools detected. Use native plugin install for Claude Code, Copilot, Droid, oh-my-pi (omp), OpenCode, Pi, and Qwen.") return } diff --git a/src/release/metadata.ts b/src/release/metadata.ts index 212b7960d..3c3730485 100644 --- a/src/release/metadata.ts +++ b/src/release/metadata.ts @@ -99,6 +99,21 @@ type GrokMarketplaceManifest = { }> } +// omp (oh-my-pi) marketplace catalog. Same shape as the Claude catalog, but +// the plugin entry carries a `version` because omp's update checker skips +// version-less catalog entries. There is no omp plugin.json — the plugin +// manifest is the existing `package.json` `pi` field. +type OmpMarketplaceManifest = { + name?: string + owner?: { name?: string } + plugins: Array<{ + name: string + version?: string + description?: string + source?: string + }> +} + type SyncOptions = { root?: string componentVersions?: Partial> @@ -588,6 +603,57 @@ export async function syncReleaseMetadata(options: SyncOptions = {}): Promise(marketplaceOmpPath) + const claudeNames = [...marketplaceClaude.plugins.map((p) => p.name)].sort() + const ompNames = [...marketplaceOmp.plugins.map((p) => p.name)].sort() + if (claudeNames.join("|") !== ompNames.join("|")) { + errors.push( + `${marketplaceOmpPath}: plugin list [${ompNames.join(", ")}] does not match ${marketplaceClaudePath} [${claudeNames.join(", ")}]`, + ) + } + let ompChanged = false + for (const plugin of marketplaceOmp.plugins) { + if (plugin.name !== "compound-engineering") continue + if (plugin.version === undefined) { + errors.push( + `${marketplaceOmpPath}: plugin "${plugin.name}" is missing required field "version". omp's update checker skips version-less catalog entries, so the plugin would never upgrade.`, + ) + } else if (plugin.version !== compoundClaude.version) { + ompChanged = true + } + if (plugin.description !== compoundMarketplaceDescription) { + plugin.description = compoundMarketplaceDescription + ompChanged = true + } + // omp resolves string sources inside the marketplace root; the plugin + // is co-located with the catalog, so the source must be "./". + if (plugin.source !== "./") { + errors.push( + `${marketplaceOmpPath}: plugin "${plugin.name}" uses source ${JSON.stringify(plugin.source)}. The plugin is co-located with the catalog, so the source must be "./".`, + ) + } + } + updates.push({ path: marketplaceOmpPath, changed: ompChanged }) + if (write && ompChanged) await writeJson(marketplaceOmpPath, marketplaceOmp) + } catch (err: unknown) { + if ((err as NodeJS.ErrnoException).code === "ENOENT") { + errors.push(`${marketplaceOmpPath} is missing but ${marketplaceClaudePath} exists. omp marketplace parity required.`) + updates.push({ path: marketplaceOmpPath, changed: false }) + } else { + throw err + } + } + // Devin manifest. Devin CLI installs the repo natively from // `.devin-plugin/plugin.json` plus the root `skills/` directory. The manifest // schema has no `skills` path field and Devin has no marketplace catalog, so diff --git a/src/utils/detect-tools.ts b/src/utils/detect-tools.ts index 0b4b1e0b5..306993ce2 100644 --- a/src/utils/detect-tools.ts +++ b/src/utils/detect-tools.ts @@ -49,6 +49,10 @@ const detectableTools: DetectableTool[] = [ name: "pi", detectPaths: (home) => [path.join(home, ".pi")], }, + { + name: "omp", + detectPaths: (home, cwd) => [path.join(home, ".omp"), path.join(cwd, ".omp")], + }, { name: "droid", detectPaths: (home) => [path.join(home, ".factory")], diff --git a/tests/ce-babysit-pr-contract.test.ts b/tests/ce-babysit-pr-contract.test.ts index 8f5a45cff..9c5fa995f 100644 --- a/tests/ce-babysit-pr-contract.test.ts +++ b/tests/ce-babysit-pr-contract.test.ts @@ -363,9 +363,12 @@ describe("ce-babysit-pr cross-skill contract parity", () => { ]) for (const text of [babysit, watchLoop]) { - expect(text).toContain("$ce-babysit-pr ") - expect(text).toContain("/ce-babysit-pr ") - expect(text).toMatch(/default to `\/ce-babysit-pr `[\s\S]{0,260}Codex[\s\S]{0,180}output one form only/i) + const renderingRule = text.match(/\*\*User-runnable resume syntax\.\*\*[^\n]+/)?.[0] + expect(renderingRule).toBeDefined() + expect(renderingRule).toContain("/ce-babysit-pr ") + expect(renderingRule).toContain("$ce-babysit-pr [posture:…]") + expect(renderingRule).not.toContain("/skill:ce-babysit-pr") + expect(renderingRule).toMatch(/Codex[\s\S]+output one form only/i) expect(text).toContain("exec ''") } }) diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 0c71acb9f..0a0977199 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -173,7 +173,7 @@ describe("CLI", () => { test("install rejects native marketplace-only plugin targets", async () => { const repoRoot = path.join(import.meta.dir, "..") - for (const target of ["copilot", "droid", "qwen"]) { + for (const target of ["copilot", "droid", "qwen", "omp"]) { const proc = Bun.spawn([ "bun", "run", @@ -1964,6 +1964,7 @@ describe("CLI", () => { await fs.mkdir(path.join(tempHome, ".config", "opencode"), { recursive: true }) await fs.mkdir(path.join(tempHome, ".codex"), { recursive: true }) await fs.mkdir(path.join(tempHome, ".pi"), { recursive: true }) + await fs.mkdir(path.join(tempHome, ".omp"), { recursive: true }) await fs.mkdir(path.join(tempHome, ".factory"), { recursive: true }) await fs.mkdir(path.join(tempHome, ".copilot"), { recursive: true }) await fs.mkdir(path.join(tempHome, ".gemini", "antigravity-cli"), { recursive: true }) @@ -2003,6 +2004,7 @@ describe("CLI", () => { expect(stdout).toContain("droid — native plugin install; skipped") expect(stdout).toContain("copilot — native plugin install; skipped") expect(stdout).toContain("qwen — native plugin install; skipped") + expect(stdout).toContain("omp — native plugin install; skipped") expect(stdout).not.toContain("cursor") expect(await exists(path.join(tempHome, ".config", "opencode", "opencode.json"))).toBe(true) diff --git a/tests/commit-push-pr-contract.test.ts b/tests/commit-push-pr-contract.test.ts index 505e49878..227a76e93 100644 --- a/tests/commit-push-pr-contract.test.ts +++ b/tests/commit-push-pr-contract.test.ts @@ -246,7 +246,8 @@ describe("PR concept teaching contract", () => { expect(trailer).toContain("using the rendering rule above") expect(trailer).toContain("$ce-explain ") expect(trailer).toContain("/ce-explain ") - expect(trailer).toMatch(/default to `\/ce-explain `[\s\S]{0,220}Codex[\s\S]{0,160}output one form only/i) + expect(trailer).not.toContain("/skill:ce-explain") + expect(trailer).toMatch(/default to `\/ce-explain `[\s\S]{0,220}Codex[\s\S]{0,260}output one form only/i) }) test("SKILL.md archival transition guards ordering, gitignore, and modes", async () => { diff --git a/tests/detect-tools.test.ts b/tests/detect-tools.test.ts index 9116e1795..336d71c60 100644 --- a/tests/detect-tools.test.ts +++ b/tests/detect-tools.test.ts @@ -47,7 +47,7 @@ describe("detectInstalledTools", () => { const results = await detectInstalledTools(tempHome, tempCwd) - expect(results.length).toBe(7) + expect(results.length).toBe(8) for (const tool of results) { expect(tool.detected).toBe(false) expect(tool.reason).toBe("not found") @@ -61,12 +61,14 @@ describe("detectInstalledTools", () => { await fs.mkdir(path.join(tempHome, ".config", "opencode"), { recursive: true }) await fs.mkdir(path.join(tempHome, ".factory"), { recursive: true }) await fs.mkdir(path.join(tempHome, ".pi"), { recursive: true }) + await fs.mkdir(path.join(tempHome, ".omp"), { recursive: true }) const results = await detectInstalledTools(tempHome, tempCwd) expect(results.find((t) => t.name === "opencode")?.detected).toBe(true) expect(results.find((t) => t.name === "droid")?.detected).toBe(true) expect(results.find((t) => t.name === "pi")?.detected).toBe(true) + expect(results.find((t) => t.name === "omp")?.detected).toBe(true) }) test("detects antigravity at ~/.gemini/antigravity-cli", async () => { diff --git a/tests/omp-native-install.test.ts b/tests/omp-native-install.test.ts new file mode 100644 index 000000000..5b9de31a6 --- /dev/null +++ b/tests/omp-native-install.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, setDefaultTimeout, test } from "bun:test" +import { readdirSync, readFileSync } from "node:fs" +import path from "node:path" +import { parseFrontmatter } from "../src/utils/frontmatter" + +// Smoke test for native oh-my-pi (omp) install of this repository (#1224). +// omp loads the repo directly from package.json#pi — no converter, no writer. +// Dry-run only: this file never installs into the user's real HOME. +setDefaultTimeout(20_000) + +const REPO_ROOT = path.join(import.meta.dir, "..") + +// Probed at module load (not beforeAll) because test.skipIf evaluates its +// condition at registration time, matching the convention in the +// *-writer.test.ts suites. +const OMP_BIN = Bun.which("omp") + +const packageJson = JSON.parse(readFileSync(path.join(REPO_ROOT, "package.json"), "utf8")) as { + name: string + version: string + pi?: { extensions?: string[]; skills?: string[]; version?: string } +} + +describe("omp native install", () => { + test.skipIf(OMP_BIN === null)("omp dry-run install recognizes the repository", async () => { + const proc = Bun.spawn([OMP_BIN!, "install", "--dry-run", "--json", REPO_ROOT], { + stdout: "pipe", + stderr: "pipe", + }) + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]) + expect(exitCode, `omp install --dry-run failed:\n${stderr}`).toBe(0) + + const result = JSON.parse(stdout) as { + name: string + manifest: { extensions?: string[]; skills?: string[]; version?: string } + } + expect(result.name).toBe("compound-engineering") + expect(result.manifest.skills).toContain("./skills") + expect(result.manifest.extensions).toContain("./.pi/extensions/compound-engineering.ts") + // The pi manifest does not pin its own version; omp then reports the + // package.json top-level version. Read both so the test drifts with the + // repo instead of pinning a literal. + const expectedVersion = packageJson.pi?.version ?? packageJson.version + expect(result.manifest.version).toBe(expectedVersion) + }) + + // The dry-run JSON above does not enumerate discovered skills, so inventory + // coverage is asserted on disk instead: omp requires a frontmatter + // description for native-provider skill discovery and silently drops skills + // without one. This test spawns nothing and stays unguarded so CI hosts + // without omp still gate the inventory. + test("omp discovers the full skill inventory", () => { + const skillsDir = path.join(REPO_ROOT, "skills") + const skillDirs = readdirSync(skillsDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + expect(skillDirs.length).toBeGreaterThan(0) + + for (const dir of skillDirs) { + const skillMdPath = path.join(skillsDir, dir, "SKILL.md") + const { data } = parseFrontmatter(readFileSync(skillMdPath, "utf8"), skillMdPath) + expect( + typeof data.name === "string" && data.name.trim() !== "", + `${dir}/SKILL.md must declare a non-empty frontmatter name`, + ).toBe(true) + expect( + typeof data.description === "string" && data.description.trim() !== "", + `${dir}/SKILL.md must declare a non-empty frontmatter description; omp drops skills without one`, + ).toBe(true) + } + }) +}) diff --git a/tests/release-metadata.test.ts b/tests/release-metadata.test.ts index 4daf38e58..3d0bd0fc3 100644 --- a/tests/release-metadata.test.ts +++ b/tests/release-metadata.test.ts @@ -27,6 +27,7 @@ async function makeFixtureRoot(): Promise { await mkdir(path.join(root, ".codex-plugin"), { recursive: true }) await mkdir(path.join(root, ".kimi-plugin"), { recursive: true }) await mkdir(path.join(root, ".grok-plugin"), { recursive: true }) + await mkdir(path.join(root, ".omp-plugin"), { recursive: true }) await mkdir(path.join(root, ".devin-plugin"), { recursive: true }) await mkdir(path.join(root, ".agents", "plugins"), { recursive: true }) @@ -158,6 +159,25 @@ async function makeFixtureRoot(): Promise { 2, ), ) + await writeFile( + path.join(root, ".omp-plugin", "marketplace.json"), + JSON.stringify( + { + name: "compound-engineering-plugin", + owner: { name: "Kieran Klaassen and Trevin Chow" }, + plugins: [ + { + name: "compound-engineering", + version: "2.42.0", + description: "old", + source: "./", + }, + ], + }, + null, + 2, + ), + ) await writeFile( path.join(root, ".claude-plugin", "marketplace.json"), JSON.stringify( @@ -507,6 +527,90 @@ describe("release metadata", () => { ]) }) + test("reports missing omp marketplace catalog as a structural error", async () => { + const root = await makeFixtureRoot() + await Bun.$`rm ${path.join(root, ".omp-plugin", "marketplace.json")}`.quiet() + + const result = await syncReleaseMetadata({ root, write: false }) + + expect(result.errors.some((err) => err.includes(".omp-plugin/marketplace.json is missing"))).toBe(true) + }) + + test("flags omp catalog plugin-version drift without rewriting the version", async () => { + const root = await makeFixtureRoot() + const ompPath = path.join(root, ".omp-plugin", "marketplace.json") + await writeFile( + ompPath, + JSON.stringify( + { + name: "compound-engineering-plugin", + plugins: [{ name: "compound-engineering", version: "2.41.0", source: "./" }], + }, + null, + 2, + ), + ) + + const result = await syncReleaseMetadata({ root, write: true }) + + // Drift is detect-only: release-please owns the version write via the + // root component's extra-files, so sync must flag but never bump it. + const ompUpdate = result.updates.find((u) => u.path === ompPath) + expect(ompUpdate).toBeDefined() + expect(ompUpdate?.changed).toBe(true) + const written = JSON.parse(await Bun.file(ompPath).text()) + expect(written.plugins[0].version).toBe("2.41.0") + }) + + test("reports omp catalog plugin entry without a version as a structural error", async () => { + const root = await makeFixtureRoot() + await writeFile( + path.join(root, ".omp-plugin", "marketplace.json"), + JSON.stringify( + { + name: "compound-engineering-plugin", + plugins: [{ name: "compound-engineering", source: "./" }], + }, + null, + 2, + ), + ) + + const result = await syncReleaseMetadata({ root, write: false }) + + expect( + result.errors.some( + (err) => + err.includes(".omp-plugin/marketplace.json") && + err.includes('missing required field "version"'), + ), + ).toBe(true) + }) + + test("reports omp marketplace plugin-list drift as a structural error", async () => { + const root = await makeFixtureRoot() + await writeFile( + path.join(root, ".omp-plugin", "marketplace.json"), + JSON.stringify( + { + name: "compound-engineering-plugin", + plugins: [{ name: "some-other-plugin", version: "1.0.0", source: "./" }], + }, + null, + 2, + ), + ) + + const result = await syncReleaseMetadata({ root, write: false }) + + expect( + result.errors.some( + (err) => err.includes(".omp-plugin/marketplace.json") && err.includes("does not match"), + ), + ).toBe(true) + }) + + test("reports Devin plugin.json name mismatch as structural error", async () => { const root = await makeFixtureRoot() await writeFile( diff --git a/tests/skill-conventions.test.ts b/tests/skill-conventions.test.ts index 8ebe88153..05757fbfa 100644 --- a/tests/skill-conventions.test.ts +++ b/tests/skill-conventions.test.ts @@ -147,14 +147,17 @@ describe("user-facing skill invocation authoring contract", () => { expect(text).toMatch(/in prose, render only the invocation as inline code[^\n]*fenced block/i) expect(text).toMatch(/active host|active harness/i) expect(text).toMatch(/default to `\/skill-name`[\s\S]{0,160}Codex[\s\S]{0,160}dollar-prefixed/i) + expect(text).toContain("`/skill:ce-polish`") + expect(text).toMatch(/oh-my-pi \(`omp`\)[^\n]*\/skill:`[^\n]*disable-model-invocation/i) expect(text).toMatch(/\/goal[\s\S]{0,180}(built-in|exception)|built-in[\s\S]{0,180}\/goal/i) expect(text).toMatch(/smallest section[\s\S]{0,180}do not repeat[\s\S]{0,180}separately loaded reference/i) } }) - test("README explains Codex invocation syntax without rewriting the built-in goal command", () => { + test("README distinguishes model-routed copy from deterministic OMP invocation", () => { expect(ROOT_README).toMatch(/README uses `\/skill-name`[\s\S]{0,180}Codex[\s\S]{0,120}`\$skill-name`/i) expect(ROOT_README).toContain("`$ce-plan` and `$lfg`") + expect(ROOT_README).toMatch(/oh-my-pi \(omp\)[^\n]*`\/skill:`[^\n]*(?:manual-only|deterministic)/i) expect(ROOT_README).toMatch(/\/goal[\s\S]{0,80}Codex built-in/i) }) }) @@ -642,6 +645,28 @@ function findPlatformVarViolations(markdown: string): PlatformVarOccurrence[] { const skillDirs = listSkillDirs() +describe("portable skill capability wording", () => { + test("does not hardcode oh-my-pi tool names", () => { + const offenders: string[] = [] + for (const skill of skillDirs) { + for (const filePath of listMarkdownFiles(skill.absPath)) { + const fileRel = path.relative(REPO_ROOT, filePath) + const lines = readFileSync(filePath, "utf8").split("\n") + for (const [index, line] of lines.entries()) { + if (/oh-my-pi[^\n]*`(?:ask|task)`|`(?:ask|task)`[^\n]*oh-my-pi/i.test(line)) { + offenders.push(`${fileRel}:${index + 1}`) + } + } + } + } + + expect( + offenders, + "Skills should describe blocking-question and subagent capabilities without adding oh-my-pi-specific tool names.", + ).toEqual([]) + }) +}) + describe("skill self-containment (AGENTS.md 'File References in Skills')", () => { for (const skill of skillDirs) { test(`${skill.relPath} has no file references escaping the skill directory`, () => { diff --git a/tests/skills/ce-explain-routing.test.ts b/tests/skills/ce-explain-routing.test.ts index 910405de3..5f7362724 100644 --- a/tests/skills/ce-explain-routing.test.ts +++ b/tests/skills/ce-explain-routing.test.ts @@ -99,8 +99,10 @@ describe("ce-explain destination and handoff routing", () => { /rendering rule above/i.test(line) && renderingRule![0].includes("$ce-polish") && renderingRule![0].includes("/ce-polish") && + renderingRule![0].includes("/skill:ce-polish") && /active host|Codex/i.test(renderingRule![0]) && - /default to `\/ce-polish`[^.]{0,180}dollar-prefixed/i.test(renderingRule![0]), + /default to `\/ce-polish`[^.]{0,180}dollar-prefixed/i.test(renderingRule![0]) && + /oh-my-pi \(`omp`\)[^\n]*\/skill:ce-polish/i.test(renderingRule![0]), "`ce-explain` SKILL.md polish handoff must present observations in chat and render one host-correct user invocation for `ce-polish`.", ).toBe(true) expect( diff --git a/tests/skills/user-facing-skill-invocation-rendering.test.ts b/tests/skills/user-facing-skill-invocation-rendering.test.ts index 3ef4763b4..4a2b225b1 100644 --- a/tests/skills/user-facing-skill-invocation-rendering.test.ts +++ b/tests/skills/user-facing-skill-invocation-rendering.test.ts @@ -6,85 +6,128 @@ function readRepoFile(relativePath: string): string { return readFileSync(path.join(process.cwd(), relativePath), "utf8") } -const rendererCases = [ +const modelVisibleRendererCases = [ { file: "skills/lfg/SKILL.md", defaults: ["/ce-explain ", "/ce-babysit-pr "], codex: ["$ce-explain ", "$ce-babysit-pr "], + unnecessaryOmp: ["/skill:ce-explain ", "/skill:ce-babysit-pr "], }, { file: "skills/ce-babysit-pr/SKILL.md", defaults: ["/ce-babysit-pr "], codex: ["$ce-babysit-pr "], + unnecessaryOmp: ["/skill:ce-babysit-pr "], }, { file: "skills/ce-babysit-pr/references/watch-loop.md", defaults: ["/ce-babysit-pr "], codex: ["$ce-babysit-pr "], + unnecessaryOmp: ["/skill:ce-babysit-pr "], }, { file: "skills/ce-commit-push-pr/SKILL.md", defaults: ["/ce-explain "], codex: ["$ce-explain "], - }, - { - file: "skills/ce-explain/SKILL.md", - defaults: ["/ce-polish"], - codex: ["$ce-polish"], - }, - { - file: "skills/ce-setup/SKILL.md", - defaults: ["/ce-setup"], - codex: ["$ce-setup"], - }, - { - file: "skills/ce-dogfood/SKILL.md", - defaults: ["/ce-setup", "/ce-dogfood "], - codex: ["$ce-setup", "$ce-dogfood "], + unnecessaryOmp: ["/skill:ce-explain "], }, { file: "skills/ce-sweep/SKILL.md", defaults: ["/lfg /plans/feedback-sweep-plan.md"], codex: ["$lfg /plans/feedback-sweep-plan.md"], - }, - { - file: "skills/ce-sweep/references/interview.md", - defaults: ["/ce-sweep"], - codex: ["$ce-sweep"], + unnecessaryOmp: ["/skill:lfg /plans/feedback-sweep-plan.md"], }, { file: "skills/ce-handoff/SKILL.md", defaults: ["/ce-handoff resume "], codex: ["$ce-handoff resume "], + unnecessaryOmp: ["/skill:ce-handoff resume "], }, { file: "skills/ce-compound/SKILL.md", defaults: ["/ce-compound-refresh ", "/ce-compound"], codex: ["$ce-compound-refresh ", "$ce-compound"], + unnecessaryOmp: ["/skill:ce-compound-refresh ", "/skill:ce-compound"], }, { file: "skills/ce-plan/references/plan-handoff.md", defaults: ["/ce-plan output:md"], codex: ["$ce-plan output:md"], + unnecessaryOmp: ["/skill:ce-plan output:md"], }, { file: "skills/ce-plan/references/universal-planning.md", defaults: ["/ce-plan"], codex: ["$ce-plan"], + unnecessaryOmp: ["/skill:ce-plan"], + }, +] as const + +const explicitOnlyRendererCases = [ + { + file: "skills/ce-explain/SKILL.md", + defaults: ["/ce-polish"], + codex: ["$ce-polish"], + omp: ["/skill:ce-polish"], + targets: ["ce-polish"], + }, + { + file: "skills/ce-setup/SKILL.md", + defaults: ["/ce-setup"], + codex: ["$ce-setup"], + omp: ["/skill:ce-setup"], + targets: ["ce-setup"], + }, + { + file: "skills/ce-dogfood/SKILL.md", + defaults: ["/ce-setup", "/ce-dogfood "], + codex: ["$ce-setup", "$ce-dogfood "], + omp: ["/skill:ce-setup", "/skill:ce-dogfood "], + targets: ["ce-setup", "ce-dogfood"], + }, + { + file: "skills/ce-sweep/references/interview.md", + defaults: ["/ce-sweep"], + codex: ["$ce-sweep"], + omp: ["/skill:ce-sweep"], + targets: ["ce-sweep"], }, ] as const describe("user-facing skill invocation rendering", () => { - test.each(rendererCases)("$file defaults to slash and reserves dollar syntax for Codex", ({ file, defaults, codex }) => { - const body = readRepoFile(file) - - expect(body).toMatch(/default(?:s| to)[^\n]*\/[a-z]/i) - expect(body).toMatch(/\$[a-z][^\n]*(?:Codex|dollar-prefixed)|(?:Codex|dollar-prefixed)[^\n]*\$[a-z]/i) - expect(body).toMatch(/Render (?:only (?:each|the) invocation as inline code|it as the fenced command below)/i) - expect(body).toMatch(/Output one form only/i) - for (const invocation of defaults) expect(body).toContain(invocation) - for (const invocation of codex) expect(body).toContain(invocation) - }) + test.each(modelVisibleRendererCases)( + "$file keeps model-visible handoffs host-neutral", + ({ file, defaults, codex, unnecessaryOmp }) => { + const body = readRepoFile(file) + + expect(body).toMatch(/default(?:s| to)[^\n]*\/[a-z]/i) + expect(body).toMatch(/\$[a-z][^\n]*(?:Codex|dollar-prefixed)|(?:Codex|dollar-prefixed)[^\n]*\$[a-z]/i) + expect(body).toMatch(/Render (?:only (?:each|the) invocation as inline code|it as the fenced command below)/i) + expect(body).toMatch(/Output one form only/i) + for (const invocation of defaults) expect(body).toContain(invocation) + for (const invocation of codex) expect(body).toContain(invocation) + for (const invocation of unnecessaryOmp) expect(body).not.toContain(invocation) + }, + ) + + test.each(explicitOnlyRendererCases)( + "$file uses deterministic OMP syntax for explicit-only skill targets", + ({ file, defaults, codex, omp, targets }) => { + const body = readRepoFile(file) + + expect(body).toMatch(/default(?:s| to)[^\n]*\/[a-z]/i) + expect(body).toMatch(/\$[a-z][^\n]*(?:Codex|dollar-prefixed)|(?:Codex|dollar-prefixed)[^\n]*\$[a-z]/i) + expect(body).toMatch(/Render (?:only (?:each|the) invocation as inline code|it as the fenced command below)/i) + expect(body).toMatch(/Output one form only/i) + expect(body).toMatch(/\/skill:[a-z][^\n]*oh-my-pi|oh-my-pi[^\n]*\/skill:[a-z]/i) + for (const target of targets) { + expect(readRepoFile(`skills/${target}/SKILL.md`)).toMatch(/^disable-model-invocation:\s*true$/m) + } + for (const invocation of defaults) expect(body).toContain(invocation) + for (const invocation of codex) expect(body).toContain(invocation) + for (const invocation of omp) expect(body).toContain(invocation) + }, + ) test("rendering rules sit at the output sections that consume them", () => { const setup = readRepoFile("skills/ce-setup/SKILL.md") @@ -126,6 +169,15 @@ describe("user-facing skill invocation rendering", () => { expect(optimize).toContain("**Run `ce-compound`**") }) + test("static ce-optimize examples use host-neutral capability wording", () => { + const usageGuide = readRepoFile("skills/ce-optimize/references/usage-guide.md") + + expect(usageGuide).toContain("Run the `ce-optimize` skill to find") + expect(usageGuide).toContain("Run the `ce-optimize` skill to improve") + expect(usageGuide).toContain("Run the `ce-optimize` skill to create") + expect(usageGuide).not.toContain("Use /ce-optimize") + }) + test("Codex goal remains a built-in exception, not a converted skill invocation", () => { const plan = readRepoFile("skills/ce-plan/SKILL.md") const planHandoff = readRepoFile("skills/ce-plan/references/plan-handoff.md")