Skip to content
Closed
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
73 changes: 73 additions & 0 deletions md/design/agent-details/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,76 @@ user's behalf); approve once via `/mcp`, or add the name to
Registration is idempotent — if the entry already exists with the
correct values, no changes are made. If the entry exists but has stale
values (e.g. the binary moved), it is updated in place.

## Plugin Delivery

A plugin enabled with `use --global` reaches Claude Code as a
**skills-directory plugin**: symposium copies the compiled plugin directory
into the user skills directory, `~/.claude/skills/<plugin>/`
(`$CLAUDE_CONFIG_DIR/skills/<plugin>/` when that variable is set, in which case
Claude Code ignores `~/.claude/skills/`). Claude Code loads every folder there
that holds `.claude-plugin/plugin.json` as the plugin `<plugin>@skills-dir`, in
place, in every project. Its skills are invoked as `/<plugin>:<skill>`.

Nothing is written to `settings.json`, `known_marketplaces.json` or
`installed_plugins.json`. A user who runs `claude plugin disable
<plugin>@skills-dir` gets an `enabledPlugins` entry that symposium never
touches.

Verified against Claude Code 2.1.284 by asking the agent (`claude plugin list
--json`, `claude plugin details`, and the `system/init` event of `claude -p`):

- The plugin is listed in the first session after `cargo agents sync`, and
`claude plugin list` shows it without a prior session.
- An edit at the same version is picked up by the next session: the copy is
read in place, and nothing is cached under `~/.claude/plugins/cache/`.
- Removing the directory removes the plugin, with no residue and no error.
- The plugin takes its identity from its manifest, not its directory name, so
a copy moved to a hashed directory name keeps its name.
- A plugin written during the `SessionStart` hook (symposium's auto-sync) loads
in the next session, or after `/reload-plugins` in the current one.
- A plain `SKILL.md` folder of the user's own next to it is unaffected.

The alternative, registering the staging root as a directory marketplace
(`extraKnownMarketplaces` plus `enabledPlugins`), also loads in place, but
Claude Code then writes `~/.claude/plugins/known_marketplaces.json` and
`installed_plugins.json` entries of its own that outlive the registration, and
at project scope two repositories using the same marketplace name loaded each
other's content.

### Project scope

Every other plugin that applies to the project reaches Claude Code the same
way, one level down: symposium copies the compiled directory from the project
staging root (`<project>/.symposium/plugins/<plugin>/`) into
`<project>/.claude/skills/<plugin>/` and adds a `.gitignore` containing `*`
inside it. Claude Code loads it as a project-scoped `<plugin>@skills-dir`, in
that project only. Nothing is written to any settings file.

Verified against Claude Code 2.1.284, logged out and offline (the `system/init`
event of `claude -p`, `claude plugin list --json`, and the TUI's `/` completion,
which shows each skill's description):

- Only in a folder the user has trusted. Untrusted, `claude -p` loads none of
them and `claude plugin list` shows `(suppressed)@skills-dir`, while plain
project skills still load. In the TUI session that accepts the trust prompt
they load only after `/reload-plugins`; later sessions load them at start.
- Repository B, with the same profile, does not see repository A's plugins.
Two repositories carrying a plugin of the same name with different content,
open at the same time, each load their own.
- The `.gitignore` inside the directory does not affect loading.
- An edit at the same version is picked up by the next session, and removing
the directory leaves nothing listed and no error.
- A plugin written during the `SessionStart` hook loads in the next session,
or after `/reload-plugins` in the current one.
- Started in a subdirectory of the project, Claude Code loads no plugin from
the root's `.claude/skills/` (`claude plugin list` is empty there), while it
still finds the plain skills in that directory, and so found the per-skill
copies symposium wrote before.
- A user-level plugin of the same name shadows the project one, and
`claude plugin list` reports it.

The Copilot CLI (1.0.89) and Goose (1.54.0) read `<project>/.claude/skills/`
too, and take the skills inside `<plugin>/skills/` as plain project skills,
as they took the per-skill copies before. Configured in the same project, each
lists every skill once, from its own copy in `.agents/skills/`.
46 changes: 26 additions & 20 deletions md/design/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,46 @@
| `opencode` | OpenCode |
| `goose` | Goose |

The agent name is stored in `[agent] name` in either the user or project config.
The agent name is stored in an `[[agent]]` entry's `name` in the user config.

## Agent responsibilities

For each agent, `cargo agents` needs to know how to:

1. **Register hooks** — write the hook configuration so the agent calls `cargo-agents hook` on the right events.
2. **Install extensions** — place skill files (and eventually workflow/MCP definitions) where the agent expects them.
2. **Install extensions**: place skills where the agent loads them, as per-skill directories in the project or as a compiled plugin directory for the user or the project.

Where these files go depends on whether the agent is configured at the user level or the project level (see [`sync --agent`](./sync-agent-flow.md)).
Hooks and MCP servers follow `hook-scope`. Skills follow the scope of whatever enabled them (see [`sync`](./sync-agent-flow.md)).

## Extension locations

When installing skills, `cargo agents` prefers vendor-neutral paths where possible:
Skills reach an agent in one of two units.

| Scope | Path | Supported by |
|-------|------|-------------|
| Project skills | `.agents/skills/<skill-name>/SKILL.md` | Antigravity, Copilot, Codex, OpenCode, Goose |
| Project skills | `.claude/skills/<skill-name>/SKILL.md` | Claude Code (does not support `.agents/skills/`) |
| Project skills | `.kiro/skills/<skill-name>/SKILL.md` | Kiro (uses its own path) |
**Per-skill directories in the project.** Everything that applies to the workspace (registry plugins active by their own gate, workspace members, dependencies, workspace-scoped `use` entries) installs one directory per skill, preferring the vendor-neutral path, on every agent that cannot take it as a project plugin:

At the project level, Claude Code requires `.claude/skills/`, Kiro requires `.kiro/skills/`, while Antigravity, Copilot, Codex, OpenCode, and Goose all support `.agents/skills/`. `cargo agents` uses the vendor-neutral `.agents/skills/` path whenever the agent supports it.
| Path | Agents |
|------|--------|
| `.agents/skills/<skill-name>/SKILL.md` | Antigravity, Copilot, Codex, OpenCode, Goose |
| `.claude/skills/<skill-name>/SKILL.md` | Claude Code (does not read `.agents/skills/`), only for a skill no compiled plugin carries |
| `.kiro/skills/<skill-name>/SKILL.md` | Kiro |

At the global level, each agent has its own path:
Symposium never writes per-skill directories at user scope.

| Agent | Global skills path |
|-------|-------------------|
| Antigravity CLI | `~/.gemini/config/skills/<skill-name>/SKILL.md` |
| Claude Code | `~/.claude/skills/<skill-name>/SKILL.md` |
| Copilot | *(no global skills path)* |
| Codex | `~/.agents/skills/<skill-name>/SKILL.md` |
| Kiro | `~/.kiro/skills/<skill-name>/SKILL.md` |
| OpenCode | `~/.agents/skills/<skill-name>/SKILL.md` |
| Goose | `~/.agents/skills/<skill-name>/SKILL.md` |
**Compiled plugin directories for the user.** A plugin that a `use --global` entry names, whatever `hook-scope` says, is compiled once into `~/.symposium/installed/<plugin>/`, an [Agent Plugins](https://agent-plugins.org/) directory carrying `plugin.json`, `.claude-plugin/plugin.json` with the same content, the `.symposium` marker and `skills/<skill>/`. Each agent that can install a plugin for the user receives it through `Agent::sync_user_plugins`, and then gets no per-skill copies of that plugin's skills:

| Agent | Delivery of a compiled plugin |
|-------|-------------------------------|
| Antigravity CLI | not yet: per-skill copies in the project |
| Claude Code | copied to `~/.claude/skills/<plugin>/` (`$CLAUDE_CONFIG_DIR/skills/` when set), loaded as `<plugin>@skills-dir` |
| Codex CLI | not yet: per-skill copies in the project |
| GitHub Copilot | not yet: per-skill copies in the project |
| Goose | not yet: per-skill copies in the project |
| Kiro | no plugin unit: per-skill copies in the project |
| OpenCode | no plugin unit: per-skill copies in the project |

**Compiled plugin directories for the project.** Every other plugin that applies to the workspace is compiled the same way into the project staging root, `<project>/.symposium/plugins/<plugin>/`, under one `.gitignore` containing `*` at `<project>/.symposium/` (written only when absent). An agent that can scope a plugin to a project receives it through `Agent::sync_project_plugins` and then gets no per-skill copies of that plugin's skills. Only Claude Code does: each directory is copied to `<project>/.claude/skills/<plugin>/`, with a `.gitignore` containing `*` added inside, and loads there as `<plugin>@skills-dir` once the user trusts the folder. Every other agent keeps the per-skill directories above for these plugins.

Symposium writes these files itself and never runs an agent's own plugin command: delivery also runs inside the `SessionStart` hook, where nothing can answer a prompt. See [Claude Code plugin delivery](./agent-details/claude-code.md#plugin-delivery) for what was verified.

---

Expand Down
22 changes: 22 additions & 0 deletions md/design/important-flows.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,28 @@ The consent prompt and the `use` / `search` / `status` commands that record deci

The key code paths are in `discovery.rs`, `config.rs` (`PluginsConfig`, `UseEntry`), `pm/cargo/mod.rs` (`active_plugins`, `load_plugin`), `plugins.rs` (`PluginManifest::requires_use`), `predicate.rs` (`PredicateContext::is_used`), and `skills.rs` (`active_plugins`, `record_active`).

## Global plugin delivery

A plugin named by a `use --global` entry is installed for the user rather than copied into each project, on every agent that can take a plugin directory. It runs inside `sync`, so `cargo agents sync`, `use`, and the hook-triggered auto-sync all do it.

1. `skills::collect_skills` stamps each applicable skill with the index of its plugin in the active set, and `sync` deduplicates the skills as before, the first plugin to reach a skill keeping it.
2. `compile::plugins_by_scope` groups the deduplicated skills by plugin and keeps for the user the plugins `compile::installs_globally` accepts: named by a `[plugins] use` entry without a workspace (`PluginsConfig::is_used_globally`), and not workspace members. `hook-scope` is not consulted. The rest form the [project set](#project-plugin-delivery).
3. `compile::compile` writes one Agent Plugins directory per plugin under `<config dir>/installed/`, writing only what changed and swapping it in by rename (`sync::sync_tree`), maintains the root's `.claude-plugin/marketplace.json`, and reaps the marked directories it did not compile (`sync::reap_unlisted`). It returns the `CompiledPlugin`s and the origins of the skills they carry.
4. For each configured agent, `Agent::sync_user_plugins` hands it the compiled set; `true` means the agent installed it, and the per-skill loop skips those skills for that agent. Claude Code copies each directory into its user skills directory (`agents/plugin_registration/claude.rs`); the other agents' modules return `false` until their delivery lands.
5. `sync::unregister_user_plugins` calls every unconfigured agent with an empty set, from `sync` and from `init`.

The key code paths are in `compile.rs`, `agents/mod.rs` (`CompiledPlugin`, `Agent::sync_user_plugins`), `agents/plugin_registration/`, `sync.rs` (`sync`, `sync_tree`, `sync_plugin_dir`, `reap_unlisted`, `unregister_user_plugins`), `skills.rs` (`SkillWithGroupContext::plugin_index`), and `config.rs` (`PluginsConfig::is_used_globally`, `INSTALLED_PLUGINS_SUBDIR`, `Symposium::env_dir`).

## Project plugin delivery

Every other applicable plugin (registry plugins active by their own gate, workspace members, dependencies, workspace-scoped `use` entries) is compiled for the project in the same `sync`, and delivered as a plugin to the agents that can scope one to a project. Claude Code is the only one; every other agent keeps per-skill copies of these plugins' skills.

1. The project set from `compile::plugins_by_scope` goes to `compile::compile_project`, which makes sure `<project>/.symposium/.gitignore` exists (containing `*`) and then runs the same `compile::compile` into the staging root `<project>/.symposium/plugins/` (`compile::project_plugins_root`). When the `.gitignore` cannot be written, it compiles nothing and every agent falls back to per-skill copies.
2. For each configured agent, `Agent::sync_project_plugins` hands it that set, after `sync_user_plugins`. Claude Code copies each directory into `<project>/.claude/skills/<plugin>/` with `sync::sync_plugin_dir`, writes a `.gitignore` containing `*` inside it (`sync::ignore_in_git`), and reaps the marked plugin directories there that it did not deliver; the other agents return `false`. A `true` makes the per-skill loop skip those skills for that agent, and the stale-skill cleanup then removes the per-skill copies earlier syncs left.
3. `sync` calls every unconfigured agent with an empty set, so their project plugins go on the project's next sync.

The key code paths are in `compile.rs` (`plugins_by_scope`, `compile_project`, `project_plugins_root`), `agents/mod.rs` (`Agent::sync_project_plugins`), `agents/plugin_registration/claude.rs`, and `sync.rs` (`sync`, `ignore_in_git`, `has_symposium_marker`).

## Help rendering

`cargo agents --help` (and `-h`, the bare `help` keyword, or no subcommand) is rendered by `help_render`, not by clap's default help.
Expand Down
Loading
Loading