diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 3f525fe6..ed8b862f 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -53,10 +53,11 @@ "category": "Productivity" }, { - "name": "speak-swiftly-server", + "name": "speak-swiftly", "source": { - "source": "local", - "path": "./plugins/SpeakSwiftlyServer" + "source": "url", + "url": "https://github.com/gaelic-ghost/SpeakSwiftlyServer.git", + "ref": "main" }, "policy": { "installation": "AVAILABLE", diff --git a/AGENTS.md b/AGENTS.md index 7e5eba3e..6a11d067 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,8 +26,8 @@ Use this file for durable repo-local guidance that Codex should follow before ch - Use a feature branch or worktree when the change needs isolation for safety, review, or overlapping parallel work, but do not force that path for ordinary `socket` maintenance. - Prefer small, focused commits over broad mixed changes. - For ordinary fixes in monorepo-owned child directories, edit the relevant copy under `plugins/` directly in `socket`. -- For `apple-dev-skills` and `SpeakSwiftlyServer`, keep subtree sync operations explicit and isolated from unrelated edits. -- Treat `plugins/SpeakSwiftlyServer` as a downstream mirror of the standalone SpeakSwiftlyServer checkout. Build, validate, tag, release, and live-refresh SpeakSwiftlyServer from its own checkout, then subtree-pull the merged child state into `socket`; do not subtree-push SpeakSwiftlyServer changes from `socket` unless Gale explicitly overrides that one-off rule. +- For `apple-dev-skills`, keep subtree sync operations explicit and isolated from unrelated edits. +- Treat `plugins/SpeakSwiftlyServer` as a downstream source mirror of the standalone SpeakSwiftlyServer checkout, not as the Socket plugin payload. Speak Swiftly plugin payload edits belong in the standalone checkout and reach Socket users through the Git-backed marketplace entry. Subtree-pull `SpeakSwiftlyServer` into `socket` only when the superproject intentionally needs the standalone source state for release or accounting work; do not subtree-push SpeakSwiftlyServer changes from `socket` unless Gale explicitly overrides that one-off rule. - When a child repo gains, removes, or moves plugin packaging, update [`.agents/plugins/marketplace.json`](./.agents/plugins/marketplace.json), [README.md](./README.md), and the root maintainer docs in the same pass. ### Subtree Sync And Branch Accounting Gates diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b0b09e5a..af343d03 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,7 @@ If the change is really about one child repository's own skills, packaging, test ### Making Changes -Keep changes bounded to one coherent root concern at a time, such as docs-only root alignment, marketplace-path or manifest-alignment fixes, root validation improvements, or root subtree-workflow documentation updates. For ordinary work in monorepo-owned child directories, edit the copy in the relevant directory under `plugins/` directly from this checkout. For `apple-dev-skills`, keep subtree pull and push operations explicit and separate from unrelated edits. For `SpeakSwiftlyServer`, treat `socket` as a pull-only mirror of the standalone release checkout unless maintainers explicitly approve a one-off push from `socket`. +Keep changes bounded to one coherent root concern at a time, such as docs-only root alignment, marketplace-path or manifest-alignment fixes, root validation improvements, or root subtree-workflow documentation updates. For ordinary work in monorepo-owned child directories, edit the copy in the relevant directory under `plugins/` directly from this checkout. For `apple-dev-skills`, keep subtree pull and push operations explicit and separate from unrelated edits. For Speak Swiftly plugin payload changes, work in the standalone `SpeakSwiftlyServer` checkout; `socket` lists that payload by Git-backed marketplace reference. Treat `plugins/SpeakSwiftlyServer` as a pull-only source mirror, and refresh it only when the superproject intentionally needs the standalone source state. When changing user-facing plugin install or update docs, make the Git-backed marketplace path the default. Use `codex plugin marketplace add /` for install setup and `codex plugin marketplace upgrade ` for updates; keep explicit refs such as `/@vX.Y.Z` scoped to pinned reproducible installs, and keep manual local marketplace roots scoped to development, unpublished testing, or fallback instructions. diff --git a/README.md b/README.md index 5e8ad213..8dfc74a3 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ Only `apple-dev-skills` and `SpeakSwiftlyServer` still use subtree sync workflow Treat Gale's local `socket` checkout as the normal day-to-day checkout on `main`. Work in the monorepo copy first, and use the relevant directory under [`plugins/`](./plugins/) for child-repository changes unless the task is explicitly about the root marketplace or root maintainer docs. Reach for a feature branch or a dedicated worktree only when the change needs extra isolation. -Keep root docs and marketplace wiring in sync with packaging changes in the same pass. For monorepo-owned child directories, edit the relevant directory under [`plugins/`](./plugins/) directly and commit in `socket`. For `apple-dev-skills` and `SpeakSwiftlyServer`, keep subtree sync operations explicit and isolated. `SpeakSwiftlyServer` is pull-only from `socket` by default: release and validate it in its standalone checkout, then pull the released state down here. +Keep root docs and marketplace wiring in sync with packaging changes in the same pass. For monorepo-owned child directories, edit the relevant directory under [`plugins/`](./plugins/) directly and commit in `socket`. For `apple-dev-skills`, keep subtree sync operations explicit and isolated. For Speak Swiftly plugin payload changes, work in the standalone `SpeakSwiftlyServer` checkout; the Socket marketplace points at that Git-backed plugin source and does not need a copied payload update here. Use the `SpeakSwiftlyServer` subtree pull only when `socket` intentionally needs the standalone source mirror refreshed for release or accounting work. When a guidance change intentionally spans multiple child skill repositories, update the affected child docs and the root `socket` docs in the same pass so the superproject still explains why the coordinated edit belongs here. @@ -128,8 +128,9 @@ Use [`docs/maintainers/release-modes.md`](./docs/maintainers/release-modes.md) f The current root validation surface is structural: - keep [`.agents/plugins/marketplace.json`](./.agents/plugins/marketplace.json) valid JSON -- verify that every listed `source.path` matches the real child surface that the child repo treats as installable -- verify that every packaged plugin path still exposes a matching `.codex-plugin/plugin.json` +- verify that every local entry's `source.path` matches the real child surface that the child repo treats as installable +- verify that every local packaged plugin path still exposes a matching `.codex-plugin/plugin.json` +- verify that every Git-backed entry uses the documented root-plugin or subdirectory source shape - review child-repo docs when plugin packaging paths move - run child-repo-specific validation from the relevant child repo when the change is really about that child repo @@ -162,6 +163,7 @@ When a child repository or helper surface grows Python-backed validation beyond │ ├── media/ │ │ └── codex-plugin-directory-socket-productivity-skills.png │ └── maintainers/ +│ ├── plugin-install-testing.md │ ├── plugin-packaging-strategy.md │ ├── release-modes.md │ └── subtree-workflow.md @@ -191,6 +193,7 @@ The root superproject docs are: - [ROADMAP.md](./ROADMAP.md) for root planning and milestone tracking - [ACCESSIBILITY.md](./ACCESSIBILITY.md) for the root accessibility contract around docs, metadata, and maintainer automation - [`docs/maintainers/`](./docs/maintainers/) for the deeper maintainer references behind the mixed-monorepo and subtree model +- [`docs/maintainers/plugin-install-testing.md`](./docs/maintainers/plugin-install-testing.md) for isolated local and Git-backed marketplace install tests that leave personal production installs untouched - [`docs/maintainers/release-modes.md`](./docs/maintainers/release-modes.md) for the `standard` and `subtrees` release modes - [`docs/media/`](./docs/media/) for README screenshots and other root documentation media assets @@ -200,11 +203,12 @@ Treat `socket` as the canonical home for the monorepo-owned child directories an - `agent-plugin-skills`, `cardhop-app`, `dotnet-skills`, `productivity-skills`, `rust-skills`, `spotify`, `things-app`, and `web-dev-skills` are monorepo-owned here. - `apple-dev-skills` and `SpeakSwiftlyServer` preserve explicit subtree sync paths. -- `SpeakSwiftlyServer` is synchronized into `socket` by subtree pull after the standalone child release lands; do not subtree-push it from `socket` unless Gale explicitly asks for that exception. +- `SpeakSwiftlyServer` may be synchronized into `socket` by subtree pull after standalone source or release work lands, but routine Speak Swiftly plugin payload edits do not need a subtree pull because the Socket catalog installs from the Git-backed standalone repository. - `python-skills` is monorepo-owned here with no separate upstream GitHub release target. - Child repos may expose plugin packaging from their own repo roots whether they are monorepo-owned here or still preserve subtree sync. - `apple-dev-skills` packages from its child-repo root at `./plugins/apple-dev-skills`, and its Codex plugin manifest registers Xcode's built-in MCP bridge through a root `.mcp.json`. - `apple-dev-skills` and `SpeakSwiftlyServer` also carry their own repo-local `.agents/plugins/marketplace.json` files so Codex can track either child repository as a Git-backed standalone marketplace without cloning `socket`. +- `SpeakSwiftlyServer` owns the canonical `speak-swiftly` plugin payload. The Socket marketplace exposes that payload by Git-backed reference so users can enable `Speak Swiftly` from the Socket catalog without `socket` carrying a second copied plugin directory. - `things-app` packages from its child-repo root at `./plugins/things-app`, and its bundled MCP server lives directly under that child repo's top-level `mcp/` directory. - `cardhop-app` packages from its child-repo root at `./plugins/cardhop-app`, and its bundled MCP server lives directly under that child repo's top-level `mcp/` directory. @@ -219,7 +223,7 @@ That marketplace points at the actual plugin root each child repository treats a - `./plugins/cardhop-app` - `./plugins/dotnet-skills` - `./plugins/productivity-skills` -- `./plugins/SpeakSwiftlyServer` +- `gaelic-ghost/SpeakSwiftlyServer` for `speak-swiftly`, displayed as `Speak Swiftly` - `./plugins/python-skills` - `./plugins/rust-skills` - `./plugins/spotify` @@ -230,6 +234,8 @@ For `things-app`, that marketplace path stays `./plugins/things-app` because the For `cardhop-app`, that marketplace path stays `./plugins/cardhop-app` because the installable plugin root is the child repo root while the bundled Cardhop MCP server now lives at top-level `mcp/` inside that child repo. +For Speak Swiftly, the marketplace points at the canonical `SpeakSwiftlyServer` repository as a Git-backed plugin source named `speak-swiftly`, with the UI display name `Speak Swiftly`. That keeps the standalone `SpeakSwiftlyServer` marketplace fully functional while avoiding two plugin payload copies that can drift. + The mixed shape is intentional for now. `socket` does not try to flatten those child repo packaging models into one fake uniform layout, and it does not define a second aggregate Codex plugin root above the child repos. Current [OpenAI Codex plugin docs](https://developers.openai.com/codex/plugins/build) support Git-backed marketplace sources and the [`codex plugin marketplace add`](https://developers.openai.com/codex/plugins/build#add-a-marketplace-from-the-cli) command. That makes the Git-backed marketplace the preferred install and update path for `socket`: @@ -241,6 +247,8 @@ codex plugin marketplace upgrade socket Use the `socket` marketplace when you want one catalog for Gale's plugin set. From that marketplace, users can install or enable individual entries such as `apple-dev-skills`, `productivity-skills`, `agent-plugin-skills`, `python-skills`, `things-app`, and the other listed child plugins. This is especially useful for workflows that need companion skills, such as Apple bootstrap or guidance-sync workflows that rely on both `apple-dev-skills` and `productivity-skills`. +When both the Socket marketplace and the standalone SpeakSwiftlyServer marketplace are configured, prefer enabling `speak-swiftly` from the Socket catalog and disabling duplicate standalone enablement. The Speak Swiftly doctor should detect duplicate installs or enablement and offer a repair path that keeps the Socket entry active. + Standalone child repositories that carry their own repo marketplace should use the same pattern against their own Git repository: ```bash diff --git a/ROADMAP.md b/ROADMAP.md index d70afab4..19f91646 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -7,7 +7,7 @@ - [Milestone Progress](#milestone-progress) - [Milestone 2: subtree workflow hardening](#milestone-2-subtree-workflow-hardening) - [Milestone 3: release and sync discipline](#milestone-3-release-and-sync-discipline) -- [Milestone 4: Speak Swiftly plugin split](#milestone-4-speak-swiftly-plugin-split) +- [Milestone 4: Speak Swiftly plugin catalog split](#milestone-4-speak-swiftly-plugin-catalog-split) - [Backlog Candidates](#backlog-candidates) - [History](#history) @@ -26,7 +26,7 @@ - Milestone 2: subtree workflow hardening - Completed - Milestone 3: release and sync discipline - Completed -- Milestone 4: Speak Swiftly plugin split - Planned +- Milestone 4: Speak Swiftly plugin catalog split - Planned ## Milestone 2: subtree workflow hardening @@ -74,36 +74,42 @@ Completed Completed Milestone 3 by aligning the release-mode docs, subtree sync rules, shared-version workflow, and roadmap backlog cleanup around the current mixed monorepo model. -## Milestone 4: Speak Swiftly plugin split +## Milestone 4: Speak Swiftly plugin catalog split ### Status -Planned +In Progress ### Scope -- [ ] Create `plugins/speak-swiftly/` as a normal monorepo-owned Codex plugin whose payload is limited to the Codex-facing surfaces: `.codex-plugin/plugin.json`, `.mcp.json`, `hooks/`, `skills/`, user guidance, and any doctor or install-check helper scripts. -- [ ] Keep `plugins/SpeakSwiftlyServer/` as a pull-only subtree mirror of the standalone Swift package while the split is underway, but stop treating that full source mirror as the preferred public Codex plugin payload. -- [ ] Update the root marketplace so the public Speak Swiftly Codex plugin entry points at `./plugins/speak-swiftly` instead of `./plugins/SpeakSwiftlyServer` once the new plugin root is validated. -- [ ] Update root README, plugin-packaging strategy, subtree workflow guidance, and child-facing docs so users understand the split: Codex users install the `speak-swiftly` plugin from `socket`; app embedders use the `SpeakSwiftlyServer` Swift package directly. -- [ ] Decide whether the `SpeakSwiftlyServer` subtree remains useful as a source mirror after the split, or whether future `socket` releases can rely on the standalone repository plus the smaller monorepo-owned plugin directory. +- [x] Keep the canonical Codex plugin payload in the standalone `SpeakSwiftlyServer` repository so `.codex-plugin/plugin.json`, `.mcp.json`, `hooks/`, `skills/`, user guidance, and doctor scripts have one source of truth. +- [x] Rename the canonical plugin identity to `speak-swiftly` while keeping the display name `Speak Swiftly`. +- [x] Update the root marketplace so the public Speak Swiftly entry is listed as `speak-swiftly` from the `gaelic-ghost/SpeakSwiftlyServer` Git-backed plugin source instead of the local `./plugins/SpeakSwiftlyServer` subtree mirror. +- [ ] Keep `plugins/SpeakSwiftlyServer/` as a pull-only subtree mirror of the standalone Swift package only for source, release, and superproject accounting while that mirror remains useful. +- [x] Update root README, plugin-packaging strategy, subtree workflow guidance, and child-facing docs so users understand the split: Codex users can install `Speak Swiftly` from either the `socket` marketplace or the standalone `SpeakSwiftlyServer` marketplace, while app embedders use the Swift package directly. ### Tickets -- [ ] Inventory the current `SpeakSwiftlyServer` plugin-only payload and copy or move the minimal files into `plugins/speak-swiftly/` without importing Swift package sources, tests, build products, or release machinery. -- [ ] Rename the plugin identity and display surface intentionally. Prefer plugin name `speak-swiftly` and display name `Speak Swiftly` unless install compatibility requires a transitional alias or migration note. -- [ ] Adapt the MCP registration and hooks so paths stay `./`-relative to the new plugin root and still target the installed local service at `127.0.0.1:7337`. -- [ ] Port or rewrite the hook doctor so it can validate the new socket-managed plugin root, the installed plugin cache, legacy global hooks, live service reachability, and expected voice profile. -- [ ] Add a migration note for users who installed `speak-swiftly-server` from `socket`, including how to install or enable `speak-swiftly` and when it is safe to remove the old plugin entry. -- [ ] Run `uv run scripts/validate_socket_metadata.py` after marketplace changes, then install or inspect the plugin through Codex to confirm the plugin directory shows the new entry. +- [x] Update `SpeakSwiftlyServer`'s plugin manifest and repo-local marketplace from `speak-swiftly-server` to `speak-swiftly`, with `interface.displayName` remaining `Speak Swiftly`. +- [x] Update `socket` marketplace validation so Git-backed remote plugin entries are allowed and checked for required metadata without requiring a local packaged plugin directory. +- [x] Change the `socket` marketplace entry from local `./plugins/SpeakSwiftlyServer` to the canonical `gaelic-ghost/SpeakSwiftlyServer` plugin source. Because the plugin root is the repository root, use the Codex marketplace source shape for a Git-backed root plugin rather than a `git-subdir` entry. +- [x] Adapt MCP registration and hooks in `SpeakSwiftlyServer` so paths stay `./`-relative to the plugin root and still target the installed local service at `127.0.0.1:7337`. +- [x] Update the hook doctor so it detects legacy `speak-swiftly-server` installs, duplicate installs from both marketplaces, plugin-managed hook state, live service reachability, and expected voice profile. +- [ ] Add doctor repair mode that prefers the `speak-swiftly@socket` install when both the `socket` marketplace and standalone `SpeakSwiftlyServer` marketplace are present, and disables or removes the duplicate standalone enablement only after reporting the intended change. +- [x] Add migration notes for users who installed `speak-swiftly-server` from either marketplace, including how to enable `speak-swiftly` from `socket` and when the old entry is safe to disable or remove. +- [x] Document isolated repo-scope install testing in `docs/maintainers/plugin-install-testing.md` so local checkout tests do not mutate Gale's personal production Codex installs. +- [ ] After this branch lands, run the Git-backed Socket marketplace install/upgrade test with a temporary `CODEX_HOME` and confirm the cached Socket catalog shows `Speak Swiftly` from the canonical `SpeakSwiftlyServer` source. ### Exit Criteria -- [ ] The `socket` marketplace exposes a small `speak-swiftly` plugin whose installable root contains only Codex plugin surfaces and intentional support docs/scripts. -- [ ] `SpeakSwiftlyServer` remains the source of truth for the Swift package, executable, LaunchAgent, embedded API, HTTP/MCP implementation, and release notes. -- [ ] User-facing docs no longer recommend installing the full `SpeakSwiftlyServer` subtree mirror from `socket` as the default Codex plugin path. -- [ ] The new plugin can be installed or enabled from the Git-backed `gaelic-ghost/socket` marketplace, and the doctor confirms hook, MCP, runtime, and voice-profile health. -- [ ] The old `speak-swiftly-server` marketplace entry is either removed, marked transitional with a documented sunset path, or intentionally retained with a clear reason. +- [x] The `socket` marketplace exposes `speak-swiftly` as a Git-backed reference to the canonical `SpeakSwiftlyServer` plugin payload instead of a second copied plugin directory. +- [x] `SpeakSwiftlyServer` remains the source of truth for the plugin payload, Swift package, executable, LaunchAgent, embedded API, HTTP/MCP implementation, and release notes. +- [x] User-facing docs no longer recommend installing the full `SpeakSwiftlyServer` subtree mirror from `socket` as the default Codex plugin path. +- [ ] `Speak Swiftly` can be installed or enabled from either the Git-backed `gaelic-ghost/socket` marketplace or the standalone `gaelic-ghost/SpeakSwiftlyServer` marketplace. +- [ ] The doctor confirms hook, MCP, runtime, and voice-profile health, detects duplicate marketplace enablement, and can repair duplicates with preference for `speak-swiftly@socket`. +- [x] The old `speak-swiftly-server` plugin id is either migrated away, marked transitional with a documented sunset path, or intentionally retained with a clear reason. + +Verified against `gaelic-ghost/SpeakSwiftlyServer` `v4.5.0`: the released repository root plugin manifest declares `name: speak-swiftly`, version `2.2.0`, `interface.displayName: Speak Swiftly`, `mcpServers: ./.mcp.json`, `hooks: ./hooks/hooks.json`, and `skills: ./skills/`; its standalone marketplace entry also exposes `speak-swiftly` from `./`. Local checkout install/remove tests for both repositories passed with temporary `CODEX_HOME` directories, leaving the test marketplaces removed. The Git-backed standalone SpeakSwiftlyServer marketplace add/upgrade test passed from isolated state; the Git-backed Socket test still needs to run after this branch lands because the current `gaelic-ghost/socket` default branch predates the new `speak-swiftly` entry. ## Backlog Candidates @@ -112,7 +118,7 @@ No active backlog candidates are currently tracked here. Add new candidates only ## History - Added root `docs/media` screenshot assets and README media guidance so the Codex plugin-directory catalog surface is visible without weakening text-first documentation. -- Planned the `Speak Swiftly` plugin split so the public Codex plugin payload can move into a small monorepo-owned `plugins/speak-swiftly/` directory instead of requiring the full `SpeakSwiftlyServer` Swift package subtree mirror. +- Planned the `Speak Swiftly` plugin catalog split so the Socket marketplace can expose the canonical `SpeakSwiftlyServer` plugin payload by Git-backed reference instead of carrying a second copied plugin directory. - Added coordinated OpenAI Codex Hooks guidance across `agent-plugin-skills` and `productivity-skills`, with future `maintain-project-hooks` work tracked in the productivity roadmap. - Updated `socket` and plugin guidance so ordinary user installs and updates default to Git-backed Codex marketplace sources and official marketplace add/upgrade commands. - Added coordinated Codex subagent guidance across `agent-plugin-skills` and `productivity-skills`, grounding skill wording in OpenAI's current explicit-trigger `subagents` model while keeping the root docs clear about why the pass belongs in `socket`. diff --git a/docs/maintainers/plugin-install-testing.md b/docs/maintainers/plugin-install-testing.md new file mode 100644 index 00000000..9de86d12 --- /dev/null +++ b/docs/maintainers/plugin-install-testing.md @@ -0,0 +1,130 @@ +# Plugin Install Testing + +Use this guide when testing the Socket marketplace or one child plugin +marketplace without touching personal production Codex installs. + +## Safety Model + +Gale's personal Codex scope should stay reserved for stable production installs. +Marketplace add, remove, and upgrade tests should use an isolated temporary +`CODEX_HOME` so test marketplaces, caches, and config do not rewrite +`~/.codex/config.toml` or the production plugin cache. + +The local checkout test and the Git-backed user-path test answer different +questions: + +- Local checkout tests prove the current branch's marketplace metadata before + the branch is merged or tagged. +- Git-backed tests prove the user-facing marketplace source that Codex can fetch + from GitHub. + +`codex plugin marketplace upgrade ` only applies to Git-backed +marketplaces. A local checkout marketplace should be added, inspected, removed, +and then discarded; trying to upgrade it should fail because it is not a Git +marketplace. + +## Socket Local Checkout Test + +Run this from the `socket` checkout when validating branch-local marketplace +changes: + +```bash +SOCKET_REPO="$(pwd)" +TEST_CODEX_HOME="$(mktemp -d /private/tmp/socket-codex-home.XXXXXX)" + +CODEX_HOME="$TEST_CODEX_HOME" codex plugin marketplace add "$SOCKET_REPO" + +jq '.plugins[] | select(.name == "speak-swiftly")' \ + "$SOCKET_REPO/.agents/plugins/marketplace.json" + +CODEX_HOME="$TEST_CODEX_HOME" codex plugin marketplace remove socket +test ! -s "$TEST_CODEX_HOME/config.toml" +rm -rf "$TEST_CODEX_HOME" +``` + +Expected result for the Speak Swiftly catalog split: + +- Codex reports an added marketplace named `socket` from the local checkout. +- The local marketplace entry contains `name: speak-swiftly`. +- The entry uses `source.source: url`, points at + `https://github.com/gaelic-ghost/SpeakSwiftlyServer.git`, and sets + `ref: main`. +- Removing `socket` leaves no configured marketplace in the temporary Codex + home. + +## Socket Git-Backed Test + +Run this after the Socket branch has landed in GitHub state that users can +fetch: + +```bash +TEST_CODEX_HOME="$(mktemp -d /private/tmp/socket-codex-home.XXXXXX)" + +CODEX_HOME="$TEST_CODEX_HOME" codex plugin marketplace add gaelic-ghost/socket +CODEX_HOME="$TEST_CODEX_HOME" codex plugin marketplace upgrade socket + +jq '.plugins[] | select(.name == "speak-swiftly")' \ + "$TEST_CODEX_HOME/.tmp/marketplaces/socket/.agents/plugins/marketplace.json" + +CODEX_HOME="$TEST_CODEX_HOME" codex plugin marketplace remove socket +test ! -s "$TEST_CODEX_HOME/config.toml" +rm -rf "$TEST_CODEX_HOME" +``` + +Expected result: + +- Codex reports `source_type = "git"` for `marketplaces.socket`. +- `upgrade socket` succeeds. +- The cached Socket marketplace contains the same `speak-swiftly` Git-backed + root-plugin entry as the local checkout. + +If the cached Socket marketplace still shows the old local +`./plugins/SpeakSwiftlyServer` entry, the test is reading an older Git revision. +Check `last_revision` in the temporary `config.toml` and wait until the intended +Socket branch has merged or install with an explicit test ref. + +## Standalone SpeakSwiftlyServer Test + +The standalone repository owns the plugin payload. Run standalone install tests +from the `SpeakSwiftlyServer` checkout and keep detailed payload validation +there. Socket tests should only prove that Socket lists the same canonical +payload by Git-backed reference. + +The current Codex CLI registers both local and Git-backed +`gaelic-ghost/SpeakSwiftlyServer` marketplaces as +`speak-swiftly-server-local`. Use the marketplace name Codex reports instead of +guessing from the repository name. + +```bash +TEST_CODEX_HOME="$(mktemp -d /private/tmp/speak-swiftly-codex-home.XXXXXX)" + +CODEX_HOME="$TEST_CODEX_HOME" codex plugin marketplace add gaelic-ghost/SpeakSwiftlyServer +CODEX_HOME="$TEST_CODEX_HOME" codex plugin marketplace upgrade speak-swiftly-server-local + +jq '.plugins[] | select(.name == "speak-swiftly")' \ + "$TEST_CODEX_HOME/.tmp/marketplaces/speak-swiftly-server-local/.agents/plugins/marketplace.json" + +jq '{name, version, displayName: .interface.displayName, mcpServers, hooks, skills}' \ + "$TEST_CODEX_HOME/.tmp/marketplaces/speak-swiftly-server-local/.codex-plugin/plugin.json" + +CODEX_HOME="$TEST_CODEX_HOME" codex plugin marketplace remove speak-swiftly-server-local +test ! -s "$TEST_CODEX_HOME/config.toml" +rm -rf "$TEST_CODEX_HOME" +``` + +Expected result: + +- The standalone marketplace entry contains `name: speak-swiftly` and + `source.path: ./`. +- The cached plugin manifest declares `name: speak-swiftly`, display name + `Speak Swiftly`, `mcpServers: ./.mcp.json`, `hooks: ./hooks/hooks.json`, and + `skills: ./skills/`. +- Removing `speak-swiftly-server-local` leaves no configured marketplace in the + temporary Codex home. + +## Session Availability + +These commands prove the marketplace and cached plugin files. They do not prove +that an already-running Codex session has refreshed its visible plugin tools and +skills. For that final check, start a fresh Codex session after the add or +upgrade and inspect the Plugin Directory or the model-visible tool list. diff --git a/docs/maintainers/plugin-packaging-strategy.md b/docs/maintainers/plugin-packaging-strategy.md index 287e0ce5..46ddb170 100644 --- a/docs/maintainers/plugin-packaging-strategy.md +++ b/docs/maintainers/plugin-packaging-strategy.md @@ -43,26 +43,22 @@ Recent monorepo-owned examples follow that rule directly: `things-app` and `card Child-repo internal layout changes do not automatically imply root marketplace changes. If a child repo keeps the same packaged plugin root, keep the `socket` marketplace path stable and only update the root docs to explain the child's new internal layout. Recent example: `things-app` keeps its marketplace path at `./plugins/things-app` while its bundled MCP server lives at top-level `mcp/` inside that child repo. -## Planned Speak Swiftly Split +## Speak Swiftly Catalog Split -`SpeakSwiftlyServer` is the exception that now needs to move away from child-root packaging in `socket`. +`SpeakSwiftlyServer` is the exception that moved away from local child-root packaging in `socket`. -The current marketplace entry points at `./plugins/SpeakSwiftlyServer`, which is the full pull-only subtree mirror of the standalone Swift package. That keeps the plugin install technically valid because the subtree root contains `.codex-plugin/plugin.json`, `.mcp.json`, `skills/`, and `hooks/`, but it also couples the public Codex plugin payload to the entire Swift package source tree, tests, maintainer docs, and release workflow. That is the wrong long-term shape for users who only want Codex to talk to an already-running local Speak Swiftly service. +The older `socket` marketplace entry pointed at `./plugins/SpeakSwiftlyServer`, which is the full pull-only subtree mirror of the standalone Swift package. That kept the plugin install technically valid because the subtree root contains `.codex-plugin/plugin.json`, `.mcp.json`, `skills/`, and `hooks/`, but it also coupled the Socket catalog entry to the entire Swift package source tree, tests, maintainer docs, and release workflow. -The planned direction is a small monorepo-owned plugin root: +The current direction is one canonical plugin payload exposed through two marketplace catalogs: -```text -plugins/speak-swiftly/ -├── .codex-plugin/ -│ └── plugin.json -├── .mcp.json -├── hooks/ -├── skills/ -├── README.md -└── scripts/ -``` +- `gaelic-ghost/SpeakSwiftlyServer` remains the canonical repository for the `speak-swiftly` plugin payload. +- `gaelic-ghost/SpeakSwiftlyServer` keeps its own repo-local marketplace so users can run `codex plugin marketplace add gaelic-ghost/SpeakSwiftlyServer`. +- `gaelic-ghost/socket` lists the same canonical plugin payload as a Git-backed root-plugin marketplace entry so users can run `codex plugin marketplace add gaelic-ghost/socket`, choose the Socket catalog, and enable `Speak Swiftly` there. +- `plugins/SpeakSwiftlyServer/` remains a pull-only subtree mirror only while it is useful for source, release, and superproject accounting. It is not the plugin payload path for Socket users. + +The plugin identity should be `speak-swiftly`, and the display name should be `Speak Swiftly`. The old `speak-swiftly-server` plugin id needs explicit migration handling because existing Codex config and plugin cache entries may still be keyed to that name. -That plugin should own only the Codex-facing distribution surface: +The canonical plugin payload in `SpeakSwiftlyServer` should own the Codex-facing distribution surface: - plugin identity, install metadata, and user-facing prompts - MCP registration for the local service endpoint @@ -71,9 +67,17 @@ That plugin should own only the Codex-facing distribution surface: - doctor or install-check scripts for plugin, hook, runtime, and voice-profile health - migration guidance from the old `speak-swiftly-server` plugin entry -The standalone `SpeakSwiftlyServer` repository should remain the source of truth for the Swift package, executable, LaunchAgent behavior, embedded API, HTTP/MCP implementation, API docs, release notes, and live-service validation. `socket/plugins/SpeakSwiftlyServer` may remain a pull-only source mirror while that is still useful for release coordination, but it should stop being the preferred public Codex plugin payload once `plugins/speak-swiftly/` exists. +The standalone `SpeakSwiftlyServer` repository should also remain the source of truth for the Swift package, executable, LaunchAgent behavior, embedded API, HTTP/MCP implementation, API docs, release notes, and live-service validation. + +The Socket catalog entry named `speak-swiftly` points at the Git-backed `gaelic-ghost/SpeakSwiftlyServer` plugin source instead of `./plugins/SpeakSwiftlyServer`. Because the plugin root is the repository root, it uses the Codex marketplace source shape for a Git-backed root plugin rather than a `git-subdir` entry. Run the marketplace audit and `uv run scripts/validate_socket_metadata.py` after changes to this entry. + +Update README, ROADMAP, subtree workflow guidance, and any SpeakSwiftlyServer-facing install docs in the same pass when this catalog model changes so users see one coherent story: Codex users can install `Speak Swiftly` from either the Git-backed `socket` marketplace or the standalone `SpeakSwiftlyServer` marketplace; app embedders use `SpeakSwiftlyServer` as a Swift package. + +For ordinary Speak Swiftly plugin changes, edit the standalone `SpeakSwiftlyServer` checkout. `socket` only needs a follow-up change when the marketplace entry itself, root docs, root validation behavior, or a coordinated `socket` release changes. Because the Socket entry tracks the standalone repository's `main` branch, a plugin payload edit in `SpeakSwiftlyServer` does not require copying files, subtree-pulling the source mirror, or bumping a Socket version by itself. + +The doctor should be able to detect and repair duplicate installs or enablement caused by users adding both marketplaces. Its repair preference is the Socket marketplace entry: keep `speak-swiftly@socket` enabled, then disable or remove the duplicate standalone-marketplace plugin enablement after reporting the intended change. The standalone marketplace remains fully functional for users who only want Speak Swiftly, but Socket is the preferred catalog when both are configured. -When the split lands, update `.agents/plugins/marketplace.json` so the public Speak Swiftly plugin entry points at `./plugins/speak-swiftly`, then run the marketplace audit and `uv run scripts/validate_socket_metadata.py`. Update README, ROADMAP, subtree workflow guidance, and any SpeakSwiftlyServer-facing install docs in the same pass so users see one coherent story: Codex users install `speak-swiftly` from the Git-backed `socket` marketplace; app embedders use `SpeakSwiftlyServer` as a Swift package. +For install-surface verification, use [plugin-install-testing.md](./plugin-install-testing.md). Keep Gale's personal Codex scope reserved for stable production installs; test local checkouts and Git-backed marketplaces with a temporary `CODEX_HOME`, remove the test marketplace before cleanup, and run the Socket-side tests from this repository while leaving detailed standalone SpeakSwiftlyServer payload checks to that repository. `socket` itself still does not define an aggregate root plugin above the child repos. The root Codex-facing surface here is the marketplace catalog, not a packaged plugin payload or a second shared plugin bundle. @@ -93,7 +97,7 @@ codex plugin marketplace add gaelic-ghost/apple-dev-skills codex plugin marketplace add gaelic-ghost/SpeakSwiftlyServer ``` -`socket` can still list the same child as `./plugins/` from the superproject marketplace. Use explicit refs such as `gaelic-ghost/socket@vX.Y.Z` only for pinned reproducible installs. Manual local marketplace roots and copied payload directories are development, unpublished-testing, and fallback tools rather than the default user-facing path. +`socket` can list ordinary local child plugins as `./plugins/` from the superproject marketplace. For Speak Swiftly, prefer a Git-backed entry pointing at `gaelic-ghost/SpeakSwiftlyServer` so the Socket catalog and standalone catalog install the same canonical payload instead of carrying two copies. Use explicit refs such as `gaelic-ghost/socket@vX.Y.Z` only for pinned reproducible installs. Manual local marketplace roots and copied payload directories are development, unpublished-testing, and fallback tools rather than the default user-facing path. When a user has already migrated from an older copied-plugin or personal-local-marketplace install to the Git-backed marketplace, use the repo-owned cleanup helper instead of hand-editing home-directory files: diff --git a/docs/maintainers/release-modes.md b/docs/maintainers/release-modes.md index cd7a9b05..0225aa42 100644 --- a/docs/maintainers/release-modes.md +++ b/docs/maintainers/release-modes.md @@ -60,14 +60,14 @@ This mode is not the same as `maintain-project-repo`'s `submodule` mode. `socket | Child | Prefix | Remote | Direction | Rule | | --- | --- | --- | --- | --- | | `apple-dev-skills` | `plugins/apple-dev-skills` | `apple-dev-skills` | pull and push | Work may start in `socket`; push back with `git subtree push` when the child repo should receive the socket-authored change. | -| `SpeakSwiftlyServer` | `plugins/SpeakSwiftlyServer` | `speak-swiftly-server` | pull-only | Build, validate, tag, release, and live-refresh in the standalone SpeakSwiftlyServer checkout, then pull the merged child state into `socket`. Do not subtree-push SpeakSwiftlyServer from `socket` unless Gale explicitly overrides this rule. | +| `SpeakSwiftlyServer` | `plugins/SpeakSwiftlyServer` | `speak-swiftly-server` | pull-only | Build, validate, tag, release, and live-refresh in the standalone SpeakSwiftlyServer checkout. Pull the merged child state into `socket` only when the superproject intentionally needs the source mirror refreshed for release or accounting work. Routine Speak Swiftly plugin payload updates flow through the Git-backed marketplace entry and do not require a subtree pull. Do not subtree-push SpeakSwiftlyServer from `socket` unless Gale explicitly overrides this rule. | ## Subtrees Mode Checklist Before opening or merging the `socket` release PR: - verify which subtree-managed children are touched -- for `SpeakSwiftlyServer`, verify the standalone checkout already owns the child release or say plainly that the socket sync is intentionally deferred +- for `SpeakSwiftlyServer`, verify whether the socket release actually needs the source mirror refreshed; if not, say plainly that plugin payload changes are served from the Git-backed standalone repository and no subtree sync is required - for `apple-dev-skills`, decide whether the socket commit must be pushed back to the child remote before the umbrella release - keep subtree sync commits isolated from unrelated docs, marketplace, or version-bump commits diff --git a/docs/maintainers/subtree-workflow.md b/docs/maintainers/subtree-workflow.md index 5569c030..3bd53171 100644 --- a/docs/maintainers/subtree-workflow.md +++ b/docs/maintainers/subtree-workflow.md @@ -24,7 +24,7 @@ For coordinated guidance that spans multiple monorepo-owned child repositories, For `apple-dev-skills`, when a change should publish back to its source repository, work in `plugins/apple-dev-skills/`, commit in `socket`, and then use `git subtree push --prefix=plugins/apple-dev-skills apple-dev-skills main`. -For `SpeakSwiftlyServer`, treat `plugins/SpeakSwiftlyServer/` as a downstream mirror. Build, validate, tag, release, and live-refresh SpeakSwiftlyServer in its standalone checkout, then use `git subtree pull --prefix=plugins/SpeakSwiftlyServer speak-swiftly-server main` to bring that released child state down into `socket`. +For `SpeakSwiftlyServer`, treat `plugins/SpeakSwiftlyServer/` as a downstream source mirror, not as the Socket plugin payload. Build, validate, tag, release, and live-refresh SpeakSwiftlyServer in its standalone checkout. Use `git subtree pull --prefix=plugins/SpeakSwiftlyServer speak-swiftly-server main` only when `socket` intentionally needs the standalone source state refreshed for release or accounting work. Routine Speak Swiftly plugin payload changes land in the standalone repository and flow to Socket users through the Git-backed marketplace entry. ## Child Repository Shape @@ -37,6 +37,20 @@ That means there is one important packaging rule to expect: The socket root marketplace must point at the actual packaged plugin root, not at an assumed one. +### Remote Catalog Entries + +Most Socket marketplace entries point at local child directories under `./plugins/`, but a marketplace entry may intentionally point at a Git-backed plugin source when the canonical payload lives outside `socket`. + +The Speak Swiftly catalog split uses this model. `SpeakSwiftlyServer` remains the canonical owner of the `speak-swiftly` plugin payload and keeps its standalone marketplace functional. The Socket marketplace exposes the same canonical plugin payload by Git-backed reference instead of installing from the local `plugins/SpeakSwiftlyServer` subtree mirror. + +Use this shape when all of these are true: + +1. The external repository is the real source of truth for the plugin manifest, skills, hooks, MCP config, and doctor scripts. +2. `socket` should list the plugin for catalog convenience, but should not own a second copied payload. +3. The local subtree mirror exists for source, release, or branch-accounting visibility rather than as the plugin install root. + +When a remote catalog entry is added, update the root marketplace validator so it understands that entry type. Local entries should still verify their checked-in packaged plugin roots. Remote entries should verify the marketplace metadata shape and document which external repository owns plugin validation. + ## Current Named Remotes The superproject keeps `origin` for `socket` and child-repository remotes for `apple-dev-skills` and `speak-swiftly-server`. @@ -135,7 +149,8 @@ Use these rules: - list every non-private imported child plugin surface by default - keep private child repos out of the public marketplace, and remove their entries if their directories are retired from the monorepo -- point `source.path` at the actual child surface the imported repo treats as installable +- point local entries' `source.path` at the actual child surface the imported repo treats as installable +- use a Git-backed source when the actual plugin payload is canonical in another repository and `socket` is only exposing it through the Socket catalog - do not change a marketplace path just because a child repo rearranged files internally; if the packaged plugin root is unchanged, keep the same `source.path` - do not invent a second socket-level plugin wrapper when the child repo already has one - do not leave stale marketplace entries behind after a packaging move or subtree removal @@ -146,14 +161,17 @@ Use these rules: Run this audit whenever a child plugin is added, removed, moved, renamed, converted from subtree-managed to monorepo-owned, or changes its packaged plugin root: 1. List every marketplace entry in `.agents/plugins/marketplace.json`. -2. For each `source.path`, verify the directory exists under `plugins/` and exposes `.codex-plugin/plugin.json` at the packaged plugin root. -3. Compare the marketplace entries against the real child directories under `plugins/` and confirm every public child plugin that ships `.codex-plugin/plugin.json` is listed. -4. Open each changed child repo's README or maintainer docs and confirm the child still treats the marketplace path as its installable plugin root. -5. Run `uv run scripts/validate_socket_metadata.py`. -6. Update `README.md`, this maintainer workflow, and `ROADMAP.md` when the audit finds a packaging-model change rather than only a metadata typo. +2. For each local `source.path`, verify the directory exists under `plugins/` and exposes `.codex-plugin/plugin.json` at the packaged plugin root. +3. For each Git-backed entry, verify the source kind matches the plugin location: `url` for a repository-root plugin and `git-subdir` for a plugin in a repository subdirectory. +4. Compare the marketplace entries against the real child directories under `plugins/` and confirm every public child plugin that ships `.codex-plugin/plugin.json` is listed or intentionally exposed by Git-backed reference. +5. Open each changed child repo's README or maintainer docs and confirm the child still treats the marketplace path as its installable plugin root. +6. Run `uv run scripts/validate_socket_metadata.py`. +7. Update `README.md`, this maintainer workflow, and `ROADMAP.md` when the audit finds a packaging-model change rather than only a metadata typo. The audit is about the installable plugin roots that Codex can actually see. Do not rewrite marketplace paths to follow an invented uniform layout when the child repo still packages from a different root. +For the Speak Swiftly catalog split, the expected surviving plugin identity is `speak-swiftly`, displayed as `Speak Swiftly`. The doctor should treat duplicate enablement from both the Socket marketplace and the standalone SpeakSwiftlyServer marketplace as repairable configuration drift. Its repair path should prefer `speak-swiftly@socket`, then disable or remove the duplicate standalone enablement after explaining the intended change. + ### Removing A Public Child Plugin Use this checklist before removing a public child repository from `socket` or from the root marketplace: diff --git a/scripts/validate_socket_metadata.py b/scripts/validate_socket_metadata.py index 49facf9b..61ddd7a8 100644 --- a/scripts/validate_socket_metadata.py +++ b/scripts/validate_socket_metadata.py @@ -13,6 +13,7 @@ REPO_ROOT = Path(__file__).resolve().parent.parent MARKETPLACE_PATH = REPO_ROOT / ".agents" / "plugins" / "marketplace.json" +GIT_SOURCE_KINDS = {"url", "git-subdir"} def fail(message: str) -> None: @@ -29,6 +30,49 @@ def load_json(path: Path) -> object: fail(f"JSON file is invalid at {path}:{exc.lineno}:{exc.colno}: {exc.msg}") +def validate_optional_git_selector( + *, + plugin_name: str, + source: dict[str, object], +) -> None: + ref = source.get("ref") + sha = source.get("sha") + if ref is not None and (not isinstance(ref, str) or not ref): + fail(f"Marketplace plugin `{plugin_name}` has an invalid Git source ref: {ref}") + if sha is not None and (not isinstance(sha, str) or not sha): + fail(f"Marketplace plugin `{plugin_name}` has an invalid Git source sha: {sha}") + if ref is not None and sha is not None: + fail(f"Marketplace plugin `{plugin_name}` must not set both Git source ref and sha.") + + +def validate_git_source( + *, + plugin_name: str, + source: dict[str, object], + source_kind: str, +) -> None: + url = source.get("url") + if not isinstance(url, str) or not url: + fail(f"Marketplace plugin `{plugin_name}` must define a non-empty Git source url.") + + validate_optional_git_selector(plugin_name=plugin_name, source=source) + + if source_kind == "url": + if "path" in source: + fail( + f"Marketplace plugin `{plugin_name}` uses a root Git source and must not " + "also define source.path. Use `git-subdir` for repository subdirectories." + ) + return + + path = source.get("path") + if not isinstance(path, str) or not path.startswith("./"): + fail( + f"Marketplace plugin `{plugin_name}` uses `git-subdir` and must define a " + f"`./...` source.path, but found `{path}`." + ) + + def validate_manifest_path( *, plugin_name: str, @@ -66,26 +110,7 @@ def validate_manifest_path( return component_path -def validate_plugin_entry(entry: object, seen_names: set[str]) -> None: - if not isinstance(entry, dict): - fail("Each marketplace plugin entry must be a JSON object.") - - name = entry.get("name") - if not isinstance(name, str) or not name: - fail("Each marketplace plugin entry must define a non-empty string `name`.") - if name in seen_names: - fail(f"Marketplace plugin name `{name}` is duplicated.") - seen_names.add(name) - - source = entry.get("source") - if not isinstance(source, dict): - fail(f"Marketplace plugin `{name}` is missing its `source` object.") - source_kind = source.get("source") - if source_kind != "local": - fail( - f"Marketplace plugin `{name}` must use a local source in this superproject, " - f"but found `{source_kind}`." - ) +def validate_local_plugin_entry(*, name: str, source: dict[str, object]) -> None: relative_path = source.get("path") if not isinstance(relative_path, str) or not relative_path.startswith("./"): fail( @@ -174,6 +199,34 @@ def validate_plugin_entry(entry: object, seen_names: set[str]) -> None: ) +def validate_plugin_entry(entry: object, seen_names: set[str]) -> None: + if not isinstance(entry, dict): + fail("Each marketplace plugin entry must be a JSON object.") + + name = entry.get("name") + if not isinstance(name, str) or not name: + fail("Each marketplace plugin entry must define a non-empty string `name`.") + if name in seen_names: + fail(f"Marketplace plugin name `{name}` is duplicated.") + seen_names.add(name) + + source = entry.get("source") + if not isinstance(source, dict): + fail(f"Marketplace plugin `{name}` is missing its `source` object.") + source_kind = source.get("source") + if source_kind == "local": + validate_local_plugin_entry(name=name, source=source) + return + if source_kind in GIT_SOURCE_KINDS: + validate_git_source(plugin_name=name, source=source, source_kind=source_kind) + return + + fail( + f"Marketplace plugin `{name}` must use a supported source kind " + f"(`local`, `url`, or `git-subdir`), but found `{source_kind}`." + ) + + def main() -> None: print("Validating root marketplace presence...") marketplace = load_json(MARKETPLACE_PATH) @@ -184,7 +237,7 @@ def main() -> None: if not isinstance(plugins, list) or not plugins: fail("Root marketplace must contain a non-empty `plugins` array.") - print("Validating packaged plugin paths...") + print("Validating marketplace entries...") seen_names: set[str] = set() for entry in plugins: validate_plugin_entry(entry, seen_names) diff --git a/tests/test_validate_socket_metadata.py b/tests/test_validate_socket_metadata.py index d3ecdaae..591e65ef 100644 --- a/tests/test_validate_socket_metadata.py +++ b/tests/test_validate_socket_metadata.py @@ -47,6 +47,30 @@ def make_marketplace_repo(tmp_path: Path, manifest: dict[str, object]) -> Path: return repo_root +def make_remote_marketplace_repo( + tmp_path: Path, + *, + source: dict[str, object], +) -> Path: + repo_root = tmp_path + write( + repo_root / ".agents" / "plugins" / "marketplace.json", + json.dumps( + { + "plugins": [ + { + "name": "speak-swiftly", + "source": source, + } + ] + }, + indent=2, + ) + + "\n", + ) + return repo_root + + def run_validator(repo_root: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(validate_socket_metadata, "REPO_ROOT", repo_root) monkeypatch.setattr( @@ -87,6 +111,72 @@ def test_main_rejects_plugin_manifest_missing_root_skills_component( run_validator(repo_root, monkeypatch) +def test_main_accepts_root_git_plugin_source( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + repo_root = make_remote_marketplace_repo( + tmp_path, + source={ + "source": "url", + "url": "https://github.com/gaelic-ghost/SpeakSwiftlyServer.git", + "ref": "main", + }, + ) + + run_validator(repo_root, monkeypatch) + + +def test_main_accepts_git_subdir_plugin_source( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + repo_root = make_remote_marketplace_repo( + tmp_path, + source={ + "source": "git-subdir", + "url": "https://github.com/example/codex-plugins.git", + "path": "./plugins/speak-swiftly", + "sha": "abc123", + }, + ) + + run_validator(repo_root, monkeypatch) + + +def test_main_rejects_root_git_source_with_path( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + repo_root = make_remote_marketplace_repo( + tmp_path, + source={ + "source": "url", + "url": "https://github.com/gaelic-ghost/SpeakSwiftlyServer.git", + "path": "./plugins/speak-swiftly", + }, + ) + + with pytest.raises(SystemExit): + run_validator(repo_root, monkeypatch) + + +def test_main_rejects_git_subdir_source_without_relative_path( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + repo_root = make_remote_marketplace_repo( + tmp_path, + source={ + "source": "git-subdir", + "url": "https://github.com/example/codex-plugins.git", + }, + ) + + with pytest.raises(SystemExit): + run_validator(repo_root, monkeypatch) + + def test_main_rejects_plugin_manifest_with_nonstandard_root_skills_component( tmp_path: Path, monkeypatch: pytest.MonkeyPatch,