Skip to content
Open
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
1 change: 1 addition & 0 deletions md/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
- [Plugin definition](./reference/plugin-definition.md)
- [Skill definition](./reference/skill-definition.md)
- [Crate predicates](./reference/crate-predicates.md)
- [Shell predicates](./reference/shell-predicates.md)
- [Contribution guide](./design/welcome.md)
- [Key repositories](./design/repositories.md)
- [Key modules](./design/module-structure.md)
Expand Down
4 changes: 4 additions & 0 deletions md/design/module-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ Defines `Source` (the `source = "..."`-tagged enum: `cargo`, `github`, `binary`)

Validates skill group source constraints at parse time: mutual exclusivity of `source.path`/`source.git`/`source.crate_path`, and the requirement that `source.crate_path` has at least one non-wildcard predicate.

### `shell_predicate.rs` — shell-command gating

Defines `ShellPredicateSet`, a list of shell commands evaluated with AND semantics. Each command runs via `sh -c <cmd>`; exit 0 means the predicate holds, any other exit (including spawn failure) means it fails. Shell predicates can be set at the plugin, skill group, skill, hook, or MCP server level. Plugin/group/skill/MCP predicates are evaluated at sync time; hook predicates are evaluated per dispatch so they observe live state. See the [shell predicates reference](../reference/shell-predicates.md).

### `skills.rs` — skill resolution and matching

Given a `PluginRegistry` and workspace dependencies, this module resolves skill group sources (fetching from git if needed), discovers `SKILL.md` files, and evaluates crate predicates at each level (plugin, group, skill) to determine which skills apply. For `source.crate_path` groups, resolves predicates to a matched crate set and fetches each crate's source via `RustCrateFetch`.
Expand Down
35 changes: 33 additions & 2 deletions md/reference/plugin-definition.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ source.path = "skills"
|-------|------|----------|-------------|
| `name` | string | yes | Plugin name. Used in logs and CLI output. |
| `crates` | string or array | no | Which crates this plugin applies to. Use `["*"]` for all crates. See [Plugin-level filtering](#plugin-level-filtering). |
| `shell_predicates` | array of strings | no | Shell commands that must all exit 0 for the plugin to apply. See [Shell predicates](./shell-predicates.md). |
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| `shell_predicates` | array of strings | no | Shell commands that must all exit 0 for the plugin to apply. See [Shell predicates](./shell-predicates.md). |
| `shell-predicates` | array of strings | no | Shell commands that must all exit 0 for the plugin to apply. See [Shell predicates](./shell-predicates.md). |

I feel like this is more "TOML-idiomatic"?

| `installations` | array of tables | no | Named installation declarations (`[[installations]]`). Hooks reference these by name. See [Installations](#installations). |
| `skills` | array of tables | no | Skill groups (`[[skills]]`). |
| `hooks` | array of tables | no | Hooks (`[[hooks]]`). |
Expand All @@ -53,7 +54,7 @@ crates = ["serde", "tokio"] # Only active in projects using serde OR tokio
crates = ["*"]
```

If omitted, the plugin applies to all projects. Plugin-level filtering is combined with skill group filtering using AND logic — both must match for skills to be available.
Plugin-level filtering is combined with skill group filtering using AND logic — both must match for skills to be available.

## `[[skills]]` groups

Expand All @@ -62,6 +63,7 @@ Each `[[skills]]` entry declares a group of skills.
| Field | Type | Description |
|-------|------|-------------|
| `crates` | string or array | Which crates this group advises on. Accepts a single string (`"serde"`) or array (`["serde", "tokio>=1.0"]`). See [Crate predicates](./crate-predicates.md) for syntax. |
| `shell_predicates` | array of strings | Shell commands that must all exit 0 for the group to install. See [Shell predicates](./shell-predicates.md). |
| `source.path` | string | Local directory containing skill subdirectories. Resolved relative to the manifest file. |
| `source.git` | string | GitHub URL pointing to a directory in a repository (e.g., `https://github.com/org/repo/tree/main/skills`). Symposium downloads the tarball, extracts the subdirectory, and caches it. |
| `source = "crate"` | string | Look for skills inside the matched crates' published source, in the default `.symposium/skills/` directory. See [Crate-sourced skills](#crate-sourced-skills). |
Expand Down Expand Up @@ -119,7 +121,7 @@ executable = "rg" # the binary to run; if omitted and the crate has a singl
args = ["--version"] # optional default args
```

Symposium attempts `cargo binstall` first, falls back to `cargo install`, and caches the result under `~/.symposium/cache/binaries/<crate>/<version>/bin/`. The chosen `executable` resolves to `<cache>/bin/<executable>`.
Symposium attempts `cargo binstall` first, falls back to `cargo install`, and caches the result under `~/.symposium/cache/binaries/<crate>/<version>/bin/` (passing `--root` so the install doesn't pollute `~/.cargo/bin`). The chosen `executable` resolves to `<cache>/bin/<executable>`. Hooks that depend on this installation get `<cache>/bin/` prepended to `$PATH`, so scripts can invoke the binary by name.

To install from a git repo instead of crates.io, set `git`:

Expand All @@ -132,6 +134,17 @@ git = "https://github.com/example/tool"
executable = "tool" # required for git sources (crates.io is not consulted)
```

To install into the user's global cargo location (`~/.cargo/bin`) instead of a symposium-managed cache, set `global = true`. No `--root` is passed; `$PATH` is not augmented (the binary is expected to already be on `$PATH`).

```toml
[[installations]]
name = "rg"
source = "cargo"
crate = "ripgrep"
executable = "rg"
global = true
```

#### `github`

```toml
Expand Down Expand Up @@ -183,6 +196,7 @@ Each `[[hooks]]` entry declares a hook that responds to agent events.
| `requirements` | array (optional) | Installations to acquire before running. Same shape as `command` (string name or inline declaration). |
| `agent` | string (optional) | Restrict the hook to a specific agent (`claude`, `copilot`, `gemini`, `kiro`, …). |
| `format` | string | Wire format for hook input/output. One of: `symposium` (default), `claude`, `codex`, `copilot`, `gemini`, `kiro`. |
| `shell_predicates` | array (optional) | Shell commands that must all exit 0 for the hook to dispatch. Evaluated per-dispatch. See [Shell predicates](./shell-predicates.md). |

### Examples

Expand Down Expand Up @@ -296,6 +310,22 @@ script = "hooks/claude/rtk-rewrite.sh"

Requirement installation is best-effort: failures are logged and dispatch continues.

### Hook environment

Hooks are spawned with the following extras on top of the parent environment:

| Variable | When set | Value |
|----------|----------|-------|
| `$SYMPOSIUM_DIR_<name>` | Installation has a symposium-managed cache (scoped cargo, github) | Absolute path to the cache / clone directory. |
| `$SYMPOSIUM_<name>` | Installation resolves to a runnable with an absolute path | Absolute path to the resolved executable / script. |
| `$PATH` | One or more dependencies contribute a runnable with an absolute path | Each runnable's parent dir is prepended, with the hook's `command` first. |

`<name>` is the installation name with non-alphanumeric characters replaced by `_` (e.g. `rtk-hooks` → `SYMPOSIUM_DIR_rtk_hooks`). Both the hook's `command` installation and every requirement (recursively, one level via installation-level requirements) contribute.

Global cargo installs (`global = true`) don't set `$SYMPOSIUM_DIR_<name>` or augment `$PATH` — the binary is expected to already be on the user's `$PATH` via `~/.cargo/bin`.

> **`install_commands` runs before env vars are set.** The `$SYMPOSIUM_*` vars and the augmented `$PATH` are only available to the hook's spawned process. `install_commands` runs earlier, inside the symposium dispatch process, so it cannot reference its own (or any other) installation's env vars. Use absolute paths in `install_commands` instead.

### Supported hook events

| Hook event | Description | CLI usage |
Expand Down Expand Up @@ -352,6 +382,7 @@ env = []
|-------|------|-------------|
| `name` | string | Server name as it appears in the agent's MCP config. |
| `crates` | string or array | Which crates this server applies to. Optional if plugin has top-level `crates`. |
| `shell_predicates` | array of strings | Shell commands that must all exit 0 for the server to register. See [Shell predicates](./shell-predicates.md). |
| `command` | string | Path to the server binary. |
| `args` | array of strings | Arguments passed to the binary. |
| `env` | array of objects | Environment variables to set when launching the server. |
Expand Down
82 changes: 82 additions & 0 deletions md/reference/shell-predicates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Shell predicates

A **shell predicate** is a shell command that decides whether a plugin, skill group, skill, hook, or MCP server should be active. Each predicate is run via `sh -c <command>`:

- exit `0` → the predicate **holds**
- any other exit (including spawn failure) → the predicate **fails**, and the enclosing item is skipped

Shell predicates compose with **AND** semantics within a list: every entry must hold. They compose with **AND** semantics across levels too, alongside the existing [crate predicates](./crate-predicates.md). Both kinds can be set independently.

## When predicates are evaluated

Shell predicates are evaluated at the same point the workspace's crate predicates are evaluated for that item:

| Level | Evaluated |
|-------|-----------|
| Plugin `shell_predicates` | At sync (gates skills & MCP) and at every hook dispatch |
| Skill group `shell_predicates` | At sync, before any git/crates source is fetched |
| Skill frontmatter `shell_predicates` | At sync, after the skill loads |
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what "after the skill loads" means. My expectation is that these predicates would be used to decide whether to "install" the skill and make it available to the agent. After the skill loads sounds to me like it would be used when the agent is activating the skill or something like that.

This suggests to me that we should make a 'lifecycle' page in the reference that gives standard terms we can reference.

| Hook `shell_predicates` | At hook dispatch, after the matcher passes |
| MCP server `shell_predicates` | At sync, when collecting servers to register |

Hook-level predicates run at dispatch (not sync) so they observe live state — e.g. a hook gated on `command -v jq` will silently disable itself if `jq` was uninstalled since the last sync, without forcing a re-sync.

> **Tip:** keep predicates **fast** and **side-effect free** (`command -v foo`, `test -f bar`, `test -d .git`). Plugin- and hook-level predicates fire on every hook dispatch.

## Usage

### Plugin manifests (TOML)

```toml
name = "my-plugin"
crates = ["*"]
shell_predicates = ["command -v rg", "test -f Cargo.toml"]

[[skills]]
crates = ["serde"]
shell_predicates = ["command -v jq"]
source = "crate"

[[hooks]]
name = "h"
event = "PreToolUse"
command = { script = "scripts/x.sh" }
shell_predicates = ["test -d .git"]

[[mcp_servers]]
name = "tool"
command = "/usr/local/bin/tool"
args = []
env = []
shell_predicates = ["command -v tool"]
```

### Skill frontmatter (YAML)

Like `crates`, `shell_predicates` is **comma-separated** on a single line in SKILL.md frontmatter:

```yaml
---
name: my-skill
description: Skill that depends on ripgrep
crates: serde
shell_predicates: command -v rg, test -f Cargo.toml
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: If front-matter is YAML, should we make this a YAML array? (I suppose the same could apply to crates) It seems more natural to me. Perhaps the answer is that you should have an option.

---
```

If you need commas inside a single command, declare the skill via a plugin manifest instead — the TOML array form supports arbitrary strings.

## Example: gating a plugin on tool availability

```toml
name = "uses-jq"
crates = ["*"]
shell_predicates = ["command -v jq"]

[[hooks]]
name = "format-json"
event = "PreToolUse"
command = { script = "scripts/format.sh" }
```

The hook here only registers if `jq` is on the user's `$PATH`. No error, no warning — symposium just skips this plugin's contributions while `jq` is missing.
1 change: 1 addition & 0 deletions md/reference/skill-definition.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Prefer deriving `Serialize` and `Deserialize` on data types.
| `name` | string | yes | Skill identifier. |
| `description` | string | yes | Short description shown in skill listings. |
| `crates` | string | no | Comma-separated crate atoms this skill is about (e.g., `crates: serde, tokio>=1.0`). Narrows the enclosing `[[skills]]` group scope — cannot widen it. |
| `shell_predicates` | string | no | Comma-separated shell commands; all must exit 0 for the skill to activate. ANDed with plugin- and group-level shell predicates. See [Shell predicates](./shell-predicates.md). |

## Crate atoms

Expand Down
Loading