Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## Unreleased

### Breaking Changes ⚠️

- Codex generated marketplaces now use `dotagents` instead of `dotagents-local`.

Existing Codex installations keep the old marketplace and plugin identities. Complete this migration after the upgrade:

1. Run `codex plugin remove <name>@dotagents-local` for each installed dotagents plugin.
1. Run `codex plugin marketplace remove dotagents-local`.
1. Run dotagents `install` or `sync` for the applicable scope.
1. Run `codex plugin marketplace add <marketplace-root>`.
1. Run `codex plugin add <name>@dotagents` for each plugin.

Dotagents does not change native Codex marketplace registration or installed plugin state.

## 3.1.0

### New Features ✨
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ The canonical portable format is an [Agent Plugins](https://agent-plugins.org/)

Global plugins install canonical bundles under `~/.agents/plugins/`. Claude and Cursor marketplaces are generated under `~/.agents/`. Copilot uses `~/.agents/.github/plugin/marketplace.json`, and Codex uses `~/.agents/plugins/marketplace.json`. Grok plugins are copied into `~/.grok/plugins/`. OpenCode skills are linked into `~/.config/opencode/skills/`, and portable MCP servers are merged into `~/.config/opencode/opencode.json`. Pi skills are linked into `~/.agents/skills/`. `--user` remains a compatibility alias for `--global`.

All generated marketplaces use the name `dotagents`. Native plugin selectors use `<name>@dotagents`.

Pi plugin targets are global skill projections rather than isolated plugin installs: a Pi-targeted plugin skill is added to `.agents/skills/` and is therefore visible to other clients that consume that shared directory.

[Pi](https://github.com/badlogic/pi-mono) reads `.agents/skills/` natively. Normal skills need no Pi-specific configuration; plugin bundles can target `pi` when their `skills/` components should be exposed there.
Expand Down
2 changes: 2 additions & 0 deletions docs/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,8 @@ Generated project-scope plugin outputs:
- OpenCode: plugin `skills/` symlinked into `.opencode/skills/`; portable `mcp.json` servers merged into `.opencode/opencode.jsonc` under `plugin.<plugin>.<server>` keys; generalized legacy plugin Markdown `agents/` symlinked into `.opencode/agents/`. Standard extension agents are preserved but not projected yet.
- Pi: plugin `skills/` symlinked into `.agents/skills/` when `pi` is a configured plugin target

Every generated marketplace uses the name `dotagents`. Native plugin selectors use `<name>@dotagents`.

Generated plugin JSON is deterministic: object keys and plugin entries are sorted, output is two-space indented, and files end with one trailing newline. Generated marketplaces and Claude, Cursor, and Codex manifests use adjacent `.dotagents-managed` sidecars so client-owned JSON remains schema-native; legacy `metadata.managedBy` output remains recognizable during migration. Managed Grok copies and OpenCode and Pi component symlinks are pruned when their plugin or target is removed. Plugin sources that resolve to this project's `.agents/plugins/<name>/` install destination are rejected so dotagents never installs a same-repo plugin onto itself. Existing plugin install destinations are overwritten only when their on-disk `.dotagents-managed` marker proves ownership.

Global plugins install under `~/.agents/plugins/`. Claude and Cursor marketplaces are generated below `~/.agents/`. Copilot uses `~/.agents/.github/plugin/marketplace.json`. Codex uses `~/.agents/plugins/marketplace.json` with paths rooted at the user's home. Grok plugins are copied into `~/.grok/plugins/`. OpenCode skills use `~/.config/opencode/skills/`, portable plugin MCP servers use `~/.config/opencode/opencode.json`, and Pi skill projections use `~/.agents/skills/`.
Expand Down
13 changes: 12 additions & 1 deletion docs/src/content/docs/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,18 @@ reinterpreting generated adapters as source input.
| `path` | string | No | Optional explicit plugin path inside the source. |
| `targets` | string[] | No | Optional subset of configured agents. |

Generated plugin outputs include Claude, Cursor, Codex, and Copilot marketplaces, native Claude, Cursor, and Codex manifests, Grok plugin directories, OpenCode skill links and portable MCP entries, and Pi skill links. OpenCode MCP keys use `plugin.<plugin>.<server>`; dotagents expands `${PLUGIN_ROOT}` and `${PLUGIN_DATA}` and preserves unrelated entries in the shared config. Generalized legacy bundles can also project Markdown agents into OpenCode; standard extension agents are preserved but not projected yet. dotagents rejects plugin sources that resolve to the same project's `.agents/plugins/<name>/` install destination. Global plugins use `~/.agents/plugins/<name>/` and global harness projections.
Generated plugin outputs include Claude, Cursor, Codex, and Copilot
marketplaces, native Claude, Cursor, and Codex manifests, Grok plugin
directories, OpenCode skill links and portable MCP entries, and Pi skill
links. Every generated marketplace uses the name `dotagents`. Native plugin
selectors use `<name>@dotagents`. OpenCode MCP keys use
`plugin.<plugin>.<server>`; dotagents expands `${PLUGIN_ROOT}` and
`${PLUGIN_DATA}` and preserves unrelated entries in the shared config.
Generalized legacy bundles can also project Markdown agents into OpenCode;
standard extension agents are preserved but not projected yet. dotagents
rejects plugin sources that resolve to the same project's
`.agents/plugins/<name>/` install destination. Global plugins use
`~/.agents/plugins/<name>/` and global harness projections.

## Scopes

Expand Down
6 changes: 6 additions & 0 deletions docs/src/content/docs/guide.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ OpenCode, and Pi locations. Override the dotagents root with `DOTAGENTS_HOME`.
`--global` selects global scope explicitly. `--user` remains a compatibility
alias.

## Generated Marketplace Name

Claude Code, Cursor, and Codex use `dotagents` for generated marketplaces. Native plugin selectors use `<name>@dotagents`.

Existing Codex installations keep the old marketplace identity after a dotagents upgrade. Remove each `<name>@dotagents-local` plugin and the `dotagents-local` marketplace. Then run dotagents `install` or `sync`, register the project or global marketplace root again, and install each plugin as `<name>@dotagents`.

<a id="configuration"></a>

## Full Configuration Example
Expand Down
10 changes: 7 additions & 3 deletions packages/dotagents/src/cli/commands/install.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ source = "path:plugin-source/review-tools"
interface: {
displayName: "Dotagents Plugins",
},
name: "dotagents-local",
name: "dotagents",
owner: {
name: "dotagents",
},
Expand Down Expand Up @@ -441,7 +441,7 @@ source = "path:plugin-source/portable-tools"
await expectHarnessFiles(projectRoot, [".agents/plugins/marketplace.json"], {
".agents/plugins/marketplace.json": { json: {
interface: { displayName: "Dotagents Plugins" },
name: "dotagents-local",
name: "dotagents",
owner: { name: "dotagents" },
plugins: [{
category: "Productivity",
Expand Down Expand Up @@ -1485,7 +1485,11 @@ source = "path:plugin-source/review-tools"
expect(existsSync(join(scope.pluginsDir, "review-tools", "plugin.json"))).toBe(true);
expect(existsSync(join(scope.root, ".claude-plugin", "marketplace.json"))).toBe(true);
expect(existsSync(join(scope.root, ".github", "plugin", "marketplace.json"))).toBe(true);
expect(existsSync(join(scope.root, ".agents", "plugins", "marketplace.json"))).toBe(true);
const codexMarketplace = JSON.parse(await readFile(
join(scope.root, ".agents", "plugins", "marketplace.json"),
"utf-8",
));
expect(codexMarketplace["name"]).toBe("dotagents");
expect(await readlink(join(scope.skillsDir, "review"))).toBe("../plugins/review-tools/skills/review");
expect((await lstat(join(userHome, ".claude", "skills"))).isSymbolicLink()).toBe(true);
expect(await readlink(join(userHome, ".claude", "skills"))).toBe(
Expand Down
16 changes: 8 additions & 8 deletions packages/dotagents/src/plugins/runtime/marketplace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type { RuntimeOutput } from "./types.js";
import { normalizePluginRuntimeLayout, type PluginRuntimeRoot } from "./layout.js";
import type { SerializedObject } from "@sentry/dotagents-lib";

const GENERATED_MARKETPLACE_NAME = "dotagents";

/** Lists managed plugin marketplace files that may be generated or pruned. */
export function marketplaceOutputPaths(root: PluginRuntimeRoot): string[] {
const layout = normalizePluginRuntimeLayout(root);
Expand Down Expand Up @@ -68,31 +70,31 @@ export function marketplaceOutputs(
outputs.push({
agent: "claude",
filePath,
content: stableJson(pathMarketplace(layout.claudeMarketplaceRoot, "dotagents", claudePlugins)),
content: stableJson(pathMarketplace(layout.claudeMarketplaceRoot, claudePlugins)),
});
}
if (copilotPlugins.length > 0) {
const filePath = layout.copilotMarketplacePath;
outputs.push({
agent: "copilot",
filePath,
content: stableJson(pathMarketplace(layout.copilotMarketplaceRoot, "dotagents", copilotPlugins)),
content: stableJson(pathMarketplace(layout.copilotMarketplaceRoot, copilotPlugins)),
});
}
if (cursorPlugins.length > 0) {
const filePath = layout.cursorMarketplacePath;
outputs.push({
agent: "cursor",
filePath,
content: stableJson(pathMarketplace(layout.cursorMarketplaceRoot, "dotagents", cursorPlugins)),
content: stableJson(pathMarketplace(layout.cursorMarketplaceRoot, cursorPlugins)),
});
}
if (codexPlugins.length > 0) {
const filePath = layout.codexMarketplacePath;
outputs.push({
agent: "codex",
filePath,
content: stableJson(codexMarketplace(layout.codexMarketplaceRoot, "dotagents-local", codexPlugins)),
content: stableJson(codexMarketplace(layout.codexMarketplaceRoot, codexPlugins)),
});
}

Expand All @@ -101,12 +103,11 @@ export function marketplaceOutputs(

function pathMarketplace(
projectRoot: string,
name: string,
plugins: PluginDeclaration[],
): SerializedObject {
return {
description: "Generated by dotagents",
name,
name: GENERATED_MARKETPLACE_NAME,
owner: {
name: "dotagents",
},
Expand Down Expand Up @@ -137,14 +138,13 @@ function pathMarketplaceEntry(

function codexMarketplace(
projectRoot: string,
name: string,
plugins: PluginDeclaration[],
): SerializedObject {
return {
interface: {
displayName: "Dotagents Plugins",
},
name,
name: GENERATED_MARKETPLACE_NAME,
owner: {
name: "dotagents",
},
Expand Down
2 changes: 1 addition & 1 deletion packages/dotagents/src/plugins/runtime/writer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ describe("plugin writer", () => {
interface: {
displayName: "Dotagents Plugins",
},
name: "dotagents-local",
name: "dotagents",
owner: {
name: "dotagents",
},
Expand Down
8 changes: 4 additions & 4 deletions skills/dotagents-qa/references/plugin-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ export CODEX_HOME="$TMP/codex-home"
mkdir -p "$CODEX_HOME"
codex plugin marketplace add "$PROJECT" --json
codex plugin list --available --json
codex plugin add qa-tools@dotagents-local --json
codex plugin add qa-tools@dotagents --json
codex plugin list --json
```

Expected evidence:

- Marketplace add returns `dotagents-local`
- Available list includes `qa-tools@dotagents-local`
- Install returns `qa-tools@dotagents-local`
- Marketplace add returns `dotagents`
- Available list includes `qa-tools@dotagents`
- Install returns `qa-tools@dotagents`
- Installed list shows the plugin enabled

Manual final check with model auth:
Expand Down
6 changes: 3 additions & 3 deletions skills/dotagents-qa/references/release-plugin-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ Validation and component inventory are no-auth proof, not model invocation.
export CODEX_HOME=/sandbox/codex-home
mkdir -p "$CODEX_HOME"
codex plugin marketplace add ./ --json
codex plugin list --marketplace dotagents-local --available --json
codex plugin add <name>@dotagents-local --json
codex plugin list --marketplace dotagents --available --json
codex plugin add <name>@dotagents --json
codex plugin list --json
```

Expand Down Expand Up @@ -135,7 +135,7 @@ copilot plugin install <name>@dotagents

cd "$HOME"
codex plugin marketplace add ./ --json
codex plugin add <name>@dotagents-local --json
codex plugin add <name>@dotagents --json

cd /sandbox/neutral-project
opencode debug skill
Expand Down
48 changes: 36 additions & 12 deletions skills/dotagents-qa/scripts/qa-example.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ Tasks:
sync-repair Delete representative generated files and assert sync repairs them
plugin-claude Validate generated Claude plugin and marketplace with Claude Code
plugin-copilot Add, install, and list the generated marketplace with Copilot CLI
plugin-codex Add/list/install generated Codex marketplace with Codex CLI
plugin-codex Add/list/install project and global Codex marketplaces
plugin-grok Confirm Grok Build discovers the generated project plugin
opencode-projections Assert generated OpenCode resource projections
plugin-clients Run every installed no-auth plugin client proof
Expand Down Expand Up @@ -345,23 +345,47 @@ async function runGrokPluginProof() {

async function runCodexPluginProof() {
prepareClientHarness("codex");
runCodexPluginLifecycle(projectDir, "project");

rmSync(dotagentsHomeDir, { recursive: true, force: true });
mkdirSync(join(dotagentsHomeDir, "plugin-source"), { recursive: true });
cpSync(
join(projectDir, "local-plugins", "qa-tools"),
join(dotagentsHomeDir, "plugin-source", "qa-tools"),
{ recursive: true },
);
writeFileSync(
join(dotagentsHomeDir, "agents.toml"),
`version = 1
agents = ["codex"]

[[plugins]]
name = "qa-tools"
source = "path:plugin-source/qa-tools"
`,
);
runCli(["install"]);
runCodexPluginLifecycle(dotagentsHomeDir, "global");
}

function runCodexPluginLifecycle(marketplaceRoot, scope) {
rmSync(codexHomeDir, { recursive: true, force: true });
mkdirSync(codexHomeDir, { recursive: true });
const env = { ...fixtureEnv, CODEX_HOME: codexHomeDir };

const add = execJson("codex", ["plugin", "marketplace", "add", projectDir, "--json"], env);
if (add.marketplaceName !== "dotagents-local") {
throw new Error("Codex marketplace add did not return dotagents-local");
const add = execJson("codex", ["plugin", "marketplace", "add", marketplaceRoot, "--json"], env);
if (add.marketplaceName !== "dotagents") {
throw new Error(`Codex ${scope} marketplace add did not return dotagents`);
}

const available = execJson("codex", ["plugin", "list", "--available", "--json"], env);
if (!available.available?.some((plugin) => plugin.pluginId === "qa-tools@dotagents-local")) {
throw new Error("Codex available plugin list did not include qa-tools@dotagents-local");
if (!available.available?.some((plugin) => plugin.pluginId === "qa-tools@dotagents")) {
throw new Error(`Codex ${scope} available plugin list did not include qa-tools@dotagents`);
}

const installed = execJson("codex", ["plugin", "add", "qa-tools@dotagents-local", "--json"], env);
if (installed.pluginId !== "qa-tools@dotagents-local") {
throw new Error("Codex plugin add did not install qa-tools@dotagents-local");
const installed = execJson("codex", ["plugin", "add", "qa-tools@dotagents", "--json"], env);
if (installed.pluginId !== "qa-tools@dotagents") {
throw new Error(`Codex ${scope} plugin add did not install qa-tools@dotagents`);
}
const installedMcp = JSON.parse(readFileSync(join(installed.installedPath, "mcp.json"), "utf-8"));
const installedMcpNames = Object.keys(installedMcp.mcpServers ?? {}).toSorted();
Expand All @@ -370,8 +394,8 @@ async function runCodexPluginProof() {
}

const list = execJson("codex", ["plugin", "list", "--json"], env);
if (!list.installed?.some((plugin) => plugin.pluginId === "qa-tools@dotagents-local" && plugin.enabled === true)) {
throw new Error("Codex installed plugin list did not include enabled qa-tools@dotagents-local");
if (!list.installed?.some((plugin) => plugin.pluginId === "qa-tools@dotagents" && plugin.enabled === true)) {
throw new Error(`Codex ${scope} installed plugin list did not include enabled qa-tools@dotagents`);
}
}

Expand Down Expand Up @@ -610,7 +634,7 @@ function assertPluginOutputs() {
assertFileIncludes("agents.lock", "qa-tools");
assertFile(".agents/plugins/marketplace.json");
assertFile(".agents/plugins/marketplace.json.dotagents-managed");
assertFileIncludes(".agents/plugins/marketplace.json", '"name": "dotagents-local"');
assertFileIncludes(".agents/plugins/marketplace.json", '"name": "dotagents"');
assertFileExcludes(".agents/plugins/marketplace.json", '"managedBy"');
assertFileIncludes(".agents/plugins/marketplace.json", '"path": "./.agents/plugins/qa-tools"');
assertFileIncludes(".agents/plugins/marketplace.json", '"installation": "AVAILABLE"');
Expand Down
2 changes: 2 additions & 0 deletions specs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ compatibility implementation (see the remaining gaps in `specs/plugins.md`):
| OpenCode | Plugin `skills/` symlinked into `.opencode/skills/`; portable `mcp.json` servers merged into `.opencode/opencode.jsonc` under `plugin.<plugin>.<server>` keys; generalized legacy plugin Markdown `agents/` symlinked into `.opencode/agents/`. Standard extension agents are preserved but not projected yet. |
| Pi | Plugin `skills/` symlinked into `.agents/skills/` when `pi` is a configured plugin target |

Every generated marketplace MUST use the name `dotagents`. Native plugin selectors use `<name>@dotagents`.

Generated plugin JSON is stable: keys are sorted, plugin entries are sorted by name, and files end with one trailing newline. Generated marketplaces and Claude, Cursor, and Codex manifests use adjacent `.dotagents-managed` sidecars; OpenCode and Pi component symlinks use marker files in reserved sibling `.dotagents-managed/` directories. This keeps ownership explicit without changing client-owned JSON or consuming a valid component name. Legacy `metadata.managedBy` output remains recognizable during migration. Managed Grok copies and component symlinks are pruned when their plugin or target is removed. Plugin sources that resolve to this project's `.agents/plugins/<name>/` install destination are rejected so dotagents never installs a same-repo plugin onto itself. Existing plugin install destinations are overwritten only when their on-disk `.dotagents-managed` marker proves ownership.

Global scope installs canonical plugins into `~/.agents/plugins/<name>/`. It generates Claude and Cursor marketplaces below `~/.agents/`, a Copilot marketplace at `~/.agents/.github/plugin/marketplace.json`, and a Codex marketplace at `~/.agents/plugins/marketplace.json`. Codex local paths start at the user's home. OpenCode skill and legacy-agent projections use `~/.config/opencode/`. Portable plugin MCP entries use `~/.config/opencode/opencode.json`. Pi skill projections use `~/.agents/skills/`.
Expand Down
2 changes: 2 additions & 0 deletions specs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,8 @@ Agent Plugin bundle
| OpenCode | Project plugin skills and merge normalized MCP servers into OpenCode config when needed | Read only namespaces registered to the OpenCode adapter | Symlink skills into `.opencode/skills/`; generalized legacy bundles may project Markdown agents, while standard extension resources are preserved but not projected yet; do not generate JavaScript or TypeScript plugin modules. |
| Pi | Project supported skills | Read only namespaces registered to the Pi adapter | Symlink skills into `.agents/skills/`; ignore unsupported MCP or extension components with warnings. |

Every generated marketplace uses the name `dotagents`. Native plugin selectors use `<name>@dotagents`.

Copilot follows its native precedence for marketplaces (`marketplace.json`,
`.plugin/marketplace.json`, then `.github/plugin/marketplace.json`) and plugin
manifests (`.plugin`, root, `.github/plugin`, then `.claude-plugin`). Dotagents
Expand Down