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
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,24 @@ jobs:

- name: Check for orphaned md files
run: cargo xtask md-orphan-check

pi-extension:
name: Check Pi extension (${{ matrix.os }})
timeout-minutes: 10
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
defaults:
run:
working-directory: src/agents/pi
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22.19.0'
cache: npm
cache-dependency-path: src/agents/pi/package-lock.json
- run: npm ci --ignore-scripts
- run: npm run check
- run: npm test
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ serde_yaml_ng = "0.10"
sha2 = "0.10"
tar = "0.4"
tempfile = "3.6"
symposium-sdk = { version = "0.1", path = "symposium-sdk", features = ["clap"] }
symposium-sdk = { version = "0.2", path = "symposium-sdk", features = ["clap"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
toml = "0.8"
tracing = "0.1"
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Which agents do you use? (space to select, enter to confirm):
[ ] Goose
[x] Kiro
[x] OpenCode
[ ] Pi
```

- **Global** hook scope (default) registers Symposium for the selected agents in your home directory, so it activates in every Rust project.
Expand Down Expand Up @@ -133,6 +134,7 @@ Every agent receives skill installation. Hook registration is available for a su
| Kiro | `.kiro/skills/` | Yes |
| OpenCode | `.agents/skills/` | No |
| Goose | `.agents/skills/` | No |
| Pi | `.agents/skills/` | Yes (TypeScript extension) |

## For crate authors

Expand Down
2 changes: 2 additions & 0 deletions md/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
- [Kiro](./reference/agents/kiro.md)
- [OpenCode](./reference/agents/opencode.md)
- [Goose](./reference/agents/goose.md)
- [Pi](./reference/agents/pi.md)
- [Configuration](./reference/configuration.md)
- [Plugin sources](./reference/plugin-source.md)
- [Plugin definition](./reference/plugin-definition.md)
Expand Down Expand Up @@ -85,6 +86,7 @@
- [Goose](./design/agent-details/goose.md)
- [Kiro](./design/agent-details/kiro.md)
- [OpenCode](./design/agent-details/opencode.md)
- [Pi](./design/agent-details/pi.md)
- [RFDs](./rfds/README.md)
- [Template](./rfds/TEMPLATE/README.md)
- [Accepted](./rfds/accepted.md) <!-- put accepted rfds in this section; the file goes in the rfds directory -->
Expand Down
12 changes: 12 additions & 0 deletions md/design/agent-details/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ The tables below summarize the answers for each agent. Individual agent pages co
| [Kiro](./kiro.md) | `.kiro/agents/*.json` | `~/.kiro/agents/*.json` | JSON, `hooks` key in agent config |
| [OpenCode](./opencode.md) | `.opencode/plugins/` | `~/.config/opencode/plugins/` | JS/TS plugins (not shell hooks) |
| [Goose](./goose.md) | *(no hooks)* | *(no hooks)* | N/A |
| [Pi](./pi.md) | `.pi/extensions/symposium.ts` | `~/.pi/agent/extensions/symposium.ts` | TypeScript extension |

### Command field

Expand All @@ -37,6 +38,7 @@ The tables below summarize the answers for each agent. Individual agent pages co
| Kiro | `command` | No |
| OpenCode | N/A (JS function) | N/A |
| Goose | N/A | N/A |
| Pi extension | `execFile("cargo-agents", ["hook", "pi", event])` | No |

### Timeout defaults

Expand All @@ -49,6 +51,7 @@ The tables below summarize the answers for each agent. Individual agent pages co
| Kiro | 30,000 | milliseconds (`timeout_ms`) |
| OpenCode | 60,000 | milliseconds (community hooks plugin) |
| Goose | N/A | N/A |
| Pi extension | 30 | seconds |

## Event names

Expand All @@ -62,6 +65,9 @@ Symposium registers hooks for four events on every agent with shell hooks, plus
| session-start | `SessionStart` | `SessionStart` | `sessionStart` | `SessionStart` | `agentSpawn` | `session.created` | N/A |
| stop | `Stop` | `Stop` | *not registered* (`agentStop`) | *not registered* (`Stop`) | *not registered* (`stop`) | N/A | N/A |

Pi uses extension events rather than shell hooks. Its mapping is documented in
[Pi integration](./pi.md#event-mapping-and-wire-format).

### Blocking support

Not all events can block the action in all agents.
Expand All @@ -75,6 +81,7 @@ Not all events can block the action in all agents.
| Kiro | Yes (exit 2) | No | No | No |
| OpenCode | Yes (throw Error) | No | No (observe only) | No (observe only) |
| Goose | N/A | N/A | N/A | N/A |
| Pi | Yes | No | No | No |

## Hook I/O protocol

Expand All @@ -89,6 +96,7 @@ Not all events can block the action in all agents.
| Kiro | `tool_name` | `tool_input` (object) | `hook_event_name`, `cwd` |
| OpenCode | `tool` | `args` (mutable output object) | `sessionID`, `callID` |
| Goose | N/A | N/A | N/A |
| Pi bridge | `tool_name` | `tool_input` (object) | `session_id`, `cwd` |

### Output structure (pre-tool-use)

Expand All @@ -101,6 +109,7 @@ Not all events can block the action in all agents.
| Kiro | *(exit code only)* | exit 0 = allow, exit 2 = block | *(not supported)* | N/A |
| OpenCode | *(throw to block)* | allow (return) / deny (throw) | mutate `output.args` | JS mutation |
| Goose | N/A | N/A | N/A | N/A |
| Pi bridge | `decision` | allow, deny | `updatedInput` | flat |

### Exit codes

Expand All @@ -127,6 +136,7 @@ All shell-based agents use the same convention (where applicable):
| Kiro | `.kiro/skills/<name>/SKILL.md` | `~/.kiro/skills/<name>/SKILL.md` |
| OpenCode | `.agents/skills/<name>/SKILL.md` | `~/.agents/skills/<name>/SKILL.md` |
| Goose | *(N/A — uses MCP extensions)* | *(N/A)* |
| Pi | `.agents/skills/<name>/SKILL.md` | `~/.agents/skills/<name>/SKILL.md` |

Symposium uses the vendor-neutral `.agents/skills/` path whenever the agent supports it, falling back to agent-specific paths (e.g., `.claude/skills/`, `.kiro/skills/`) when required. Codex CLI and OpenCode also support `.agents/skills/` natively.

Expand All @@ -141,6 +151,7 @@ Symposium uses the vendor-neutral `.agents/skills/` path whenever the agent supp
| Kiro | `.kiro/steering/*.md`, `AGENTS.md` | `~/.kiro/steering/*.md` |
| OpenCode | `AGENTS.md`, `CLAUDE.md` | `~/.config/opencode/AGENTS.md` |
| Goose | `.goosehints`, `AGENTS.md` | `~/.config/goose/.goosehints` |
| Pi | `AGENTS.md`, `CLAUDE.md` | `~/.pi/agent/AGENTS.md` |

## MCP server configuration

Expand All @@ -162,6 +173,7 @@ symposium reports success for a file the agent never reads.
| GitHub Copilot CLI | *(none - user scope only)* | `~/.copilot/mcp-config.json` | `mcpServers.<name>` = `{command, args}` | yes |
| Kiro | `.kiro/settings/mcp.json` | `~/.kiro/settings/mcp.json` | `mcpServers.<name>` = `{command, args}` | no (GUI only) |
| Goose | *(none - user scope only)* | `~/.config/goose/config.yaml` | `extensions.<name>` = `{name, type: stdio, cmd, args, enabled, envs}` | yes |
| Pi | `.pi/mcp.json` | `~/.pi/agent/mcp.json` | `mcpServers.<name>` = `{command, args}` or `{url, headers}` | yes (`pi mcp list`) |

Notes that cost real debugging time:

Expand Down
108 changes: 108 additions & 0 deletions md/design/agent-details/pi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Pi integration

Primary sources: [Pi extensions](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md),
[skills](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/skills.md),
and [MCP](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/mcp.md).

## Extension registration

`src/agents/pi.rs` installs the embedded `src/agents/pi/extension.ts` into
`.pi/extensions/symposium.ts` at project scope or
`~/.pi/agent/extensions/symposium.ts` at user scope. The user path honors
`PI_CODING_AGENT_DIR`. Registration compares content before writing. The generated
header identifies files that Symposium may update or remove. An existing file
without that header is not overwritten or removed. When hook scope changes,
`sync` calls `Agent::register_scoped_hooks`, which removes the generated extension
at the other scope for that workspace.
Run `/reload` after a scope change to replace the active handlers.

The extension starts no work in its factory. Every handler checks
`ctx.isProjectTrusted()` before dispatch or project resource discovery, including
when the extension is installed globally. Event handlers run `cargo-agents
hook pi <event>` with JSON on stdin, using `execFile` without a shell. Prompts and
tool inputs are not shell arguments. Each process has a 10-minute timeout and a
1 MiB output limit. All events use this timeout because tools install lazily when
their hook first fires; session-start refresh does not install missing tools.
Active-turn cancellation stops its hook process.

## Event mapping and wire format

| Pi event | Symposium event |
|----------|-----------------|
| `session_start` | `SessionStart` |
| `input` | `UserPromptSubmit` |
| `tool_call` | `PreToolUse` |
| `tool_result` | `PostToolUse` |
| `agent_end` | `Stop` |

`src/hook_schema/pi.rs` uses the SDK's event-specific structs as the Pi bridge
wire format. The command-line event name supplies the tag. Each input carries
`cwd` and `session_id`; tool events also carry `tool_name` and `tool_input`.
Post-tool input includes Pi's content, details, structured content, and error
state in `tool_response`.

Output uses the SDK's flat fields: `decision`, `updatedInput`, and
`additionalContext`. Denial becomes Pi's `{ block: true, reason }`. Input updates
replace the existing input object's keys. Only an explicit `decision = "deny"`
blocks a tool. Process failures, missing binaries, timeouts, malformed output,
and non-object input updates let the tool run. Only the first hook error in each
session produces a warning. Cancellation produces no warning and does not count
as that first error. Warning state resets on `session_start`, not on turn end.
Warnings use Pi's UI when available, or stderr otherwise; they never use protocol
stdout.

Pre-tool context is stored by `toolCallId` and appended to that tool's result,
followed by any post-tool context. This lets the model read it on its next request
without waiting for another user prompt. The result keeps its structured content
and error state. A post-hook failure does not discard pre-tool context. Pending
context is removed after the result and cleared on turn end, session start, or
session shutdown.

Session-start, prompt, and Stop context use hidden custom messages for the next
user prompt, without requesting another turn.

Symposium's normal hook pipeline handles auto-sync and plugin format conversion.
Native `format = "pi"` plugins receive flat Pi bridge JSON; portable
`format = "symposium"` plugins receive tagged SDK JSON.

## Generated skill discovery

Issue [#248](https://github.com/symposium-dev/symposium/issues/248) identifies a
Pi-specific limit: Pi's directory scanner honors `.gitignore`, including the `*`
rule Symposium writes in generated skill directories.

Keep the shared `.agents/skills/` path and its ignore rules. Pi fires
`resources_discover` after `session_start`, so automatic sync installs skills
before the extension returns their explicit `SKILL.md` paths. Explicit files
bypass the directory scan. The extension checks the current directory and its
ancestors up to the repository root, plus `~/.agents/skills/`. Without a repository
root, it checks ancestors up to the filesystem root. It returns only
skills with the `.symposium` marker. User skills still follow Pi's normal rules.

A dependency change can install more skills during an active session. Pi needs
`/reload` to discover new resources and MCP entries. Removing a skill removes it
from the next resource-discovery result.

## MCP registration

Pi 1.0 reads `mcpServers` from `.pi/mcp.json` and
`~/.pi/agent/mcp.json`. Entries use `command`, `args`, and `env` for stdio, or
`url` and `headers` for streamable HTTP. `register_pi_mcp_servers` updates the
transport fields while preserving Pi-specific user options. SSE is unsupported
and is skipped with a warning.

## Tests

```bash
cargo test --test pi --test hook_context
cd src/agents/pi
npm ci --ignore-scripts
npm run check
npm test
```

Rust tests cover init, scope selection, automatic sync, cleanup, wire conversion,
and MCP updates. Node tests cover event handling, subprocess errors, input
changes, parallel calls, pre-tool context timing, warning-only hook failures,
skill discovery using Pi's real skill loader, and MCP file discovery using
`pi mcp list` with local test servers.
45 changes: 32 additions & 13 deletions md/design/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
| `kiro` | Kiro |
| `opencode` | OpenCode |
| `goose` | Goose |
| `pi` | Pi |

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

Expand All @@ -31,11 +32,11 @@ When installing skills, `cargo agents` prefers vendor-neutral paths where possib

| Scope | Path | Supported by |
|-------|------|-------------|
| Project skills | `.agents/skills/<skill-name>/SKILL.md` | Antigravity, Copilot, Codex, OpenCode, Goose |
| Project skills | `.agents/skills/<skill-name>/SKILL.md` | Antigravity, Copilot, Codex, OpenCode, Goose, Pi |
| 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) |

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.
At the project level, Claude Code requires `.claude/skills/`, Kiro requires `.kiro/skills/`, while Antigravity, Copilot, Codex, OpenCode, Goose, and Pi all support `.agents/skills/`. `cargo agents` uses the vendor-neutral `.agents/skills/` path whenever the agent supports it.

At the global level, each agent has its own path:

Expand All @@ -48,6 +49,7 @@ At the global level, each agent has its own path:
| Kiro | `~/.kiro/skills/<skill-name>/SKILL.md` |
| OpenCode | `~/.agents/skills/<skill-name>/SKILL.md` |
| Goose | `~/.agents/skills/<skill-name>/SKILL.md` |
| Pi | `~/.agents/skills/<skill-name>/SKILL.md` |

---

Expand Down Expand Up @@ -395,17 +397,33 @@ Goose is supported as a **skills-only** agent — `cargo agents sync` will insta

---

## Pi

[Integration reference](./agent-details/pi.md)

Pi uses TypeScript extensions. Symposium installs `symposium.ts` in
`.pi/extensions/` or `~/.pi/agent/extensions/`, according to hook scope. It maps
session, input, tool, and turn-end events to `cargo-agents hook pi` calls. The
bridge supports automatic sync, context, tool denial, and tool-input changes.

Pi's skill scanner respects `.gitignore`, which hides generated skills. The
extension supplies explicit paths to managed `SKILL.md` files through
`resources_discover`. This event runs after session-start sync. The shared
`.agents/skills/` path and its ignore rules remain unchanged.

---

## Cross-agent event mapping

The following table maps symposium's internal event names to each agent's wire-format event name. `—` means the agent does not support shell-command hooks; *not registered* means symposium does not hook that event there.

| Symposium event | Antigravity | Claude | Copilot | Codex | Kiro | OpenCode | Goose |
|---|---|---|---|---|---|---|---|
| `pre-tool-use` | `PreToolUse` | `PreToolUse` | `preToolUse` | `PreToolUse` | `preToolUse` | — | — |
| `post-tool-use` | `PostToolUse` | `PostToolUse` | `postToolUse` | `PostToolUse` | `postToolUse` | — | — |
| `user-prompt-submit` | `PreInvocation` | `UserPromptSubmit` | `userPromptSubmitted` | `UserPromptSubmit` | `userPromptSubmit` | — | — |
| `session-start` | `SessionStart` | `SessionStart` | `sessionStart` | `SessionStart` | `agentSpawn` | — | — |
| `stop` | `Stop` | `Stop` | *not registered* | *not registered* | *not registered* | — | — |
| Symposium event | Antigravity | Claude | Copilot | Codex | Kiro | OpenCode | Goose | Pi extension |
|---|---|---|---|---|---|---|---|---|
| `pre-tool-use` | `PreToolUse` | `PreToolUse` | `preToolUse` | `PreToolUse` | `preToolUse` | — | — | `tool_call` |
| `post-tool-use` | `PostToolUse` | `PostToolUse` | `postToolUse` | `PostToolUse` | `postToolUse` | — | — | `tool_result` |
| `user-prompt-submit` | `PreInvocation` | `UserPromptSubmit` | `userPromptSubmitted` | `UserPromptSubmit` | `userPromptSubmit` | — | — | `input` |
| `session-start` | `SessionStart` | `SessionStart` | `sessionStart` | `SessionStart` | `agentSpawn` | — | — | `session_start` |
| `stop` | `Stop` | `Stop` | *not registered* | *not registered* | *not registered* | — | — | `agent_end` |

Tool names are each agent's own, and symposium passes them to plugin hooks unchanged: the same shell tool is `Bash` in Claude Code and Codex, `bash` in Copilot and `run_command` in Antigravity. A `matcher` that should fire on several agents has to name each of them.

Expand All @@ -415,7 +433,8 @@ Tool names are each agent's own, and symposium passes them to plugin hooks uncha

To add support for a new agent:

1. Add a variant to the `HookAgent` enum in `hook_schema.rs`.
2. Create an agent module (e.g., `hook_schema/newagent.rs`) implementing the `Agent` trait and the event-specific payload/output types.
3. Implement the `AgentHookPayload` and `AgentHookOutput` traits to convert between the agent's wire format and the internal `HookPayload`/`HookOutput` types.
4. Document the agent's hook registration locations and extension file layout in this page.
1. Add variants to `agents::Agent` and `symposium_sdk::hook::HookAgent`.
2. Add skill paths, hook registration, and MCP configuration to `agents`.
3. Create a `hook_schema` module that implements the `Agent` trait and the event-specific input/output types. Add its handler to `hook_schema::agent_event`.
4. Implement `AgentHookInput` and `AgentHookOutput` to convert the wire format to and from the SDK types.
5. Test init, sync, cleanup, and hook dispatch. Document the agent's file layout and protocol.
Loading