# Plugin Architecture
Catenary ships plugins for two AI CLI hosts from a single repository. Each host
has its own plugin format and file layout, but they share the same `catenary`
binary and MCP server.
## Repository Layout
```
Catenary/
├── .claude-plugin/
│ └── marketplace.json # Claude Code marketplace metadata
├── plugins/
│ └── catenary/ # Claude Code plugin root
│ ├── .mcp.json # MCP server declaration
│ ├── hooks/
│ │ └── hooks.json # Claude Code hooks
│ ├── config.example.toml
│ └── README.md
├── gemini-extension.json # Gemini CLI extension manifest
├── hooks/
│ └── hooks.json # Gemini CLI hooks
└── ...
```
The two plugin roots are:
| Claude Code | `plugins/catenary/` | `plugins/catenary/hooks/hooks.json` |
| Gemini CLI | repo root (`/`) | `hooks/hooks.json` |
Both hosts expect hooks in a `hooks/hooks.json` file relative to the plugin
root. The manifest file (where the MCP server is declared) is separate from the
hooks file in both cases.
## Claude Code Plugin
Installed via the marketplace:
```bash
claude plugin marketplace add MarkWells-Dev/Catenary
claude plugin install catenary@catenary
```
`.claude-plugin/marketplace.json` points to the plugin source directory:
```json
"source": "./plugins/catenary"
```
Inside `plugins/catenary/`:
- **`.mcp.json`** — declares the MCP server (`catenary` command).
- **`hooks/hooks.json`** — registers hooks for diagnostics, root sync, and
file locking:
- `PreToolUse` (all tools): runs `catenary sync-roots` to pick up `/add-dir`
workspace additions.
- `PreToolUse` on `Edit|Write|NotebookEdit`: runs `catenary lock acquire` to
serialize concurrent edits across agents.
- `PostToolUse` on `Edit|Write|NotebookEdit`: runs `catenary notify` for
post-edit LSP diagnostics, then `catenary lock release` to start the grace
period.
- `PostToolUse` on `Read`: runs `catenary lock track-read` for change
detection.
- `PostToolUseFailure` on `Edit|Write|NotebookEdit`: runs
`catenary lock release --grace 0` for immediate lock release on failure.
- **`config.example.toml`** — example Catenary configuration.
## Gemini CLI Extension
Installed via:
```bash
gemini ext install markwells.catenary
```
The extension root is the repository root. Two files matter:
- **`gemini-extension.json`** — manifest declaring the MCP server. Does **not**
contain hooks (Gemini CLI ignores hooks defined in the manifest).
- **`hooks/hooks.json`** — registers hooks for diagnostics and file locking:
- `BeforeTool` on `write_file|replace`: runs
`catenary lock acquire --format=gemini` to serialize concurrent edits.
- `AfterTool` on `read_file|write_file|replace`: runs
`catenary notify --format=gemini` for post-edit LSP diagnostics.
- `AfterTool` on `write_file|replace`: runs
`catenary lock release --format=gemini` for lock grace period.
- `AfterTool` on `read_file`: runs
`catenary lock track-read --format=gemini` for change detection.
## Hook Contracts
All hook commands (`catenary notify`, `catenary sync-roots`, `catenary lock`)
read hook JSON from stdin. They silently succeed on any error to avoid breaking
the host CLI's flow.
### `catenary notify`
Triggered after file edits. Reads the hook JSON, extracts the file path, finds
the matching Catenary session, and returns LSP diagnostics to stdout.
**Fields consumed from hook JSON:**
| `tool_input.file_path` or `tool_input.file` | File that was edited |
| `cwd` | Resolving relative file paths (fallback: process CWD) |
**Output format** depends on the `--format` flag:
- Default (Claude Code): plain text, one diagnostic per line.
- `--format=gemini`: wrapped in `<hook_context>` tags for Gemini's context
injection.
### `catenary sync-roots`
Triggered before each tool use (Claude Code only). Scans the Claude Code
transcript for `/add-dir` confirmations and sends newly discovered roots to the
running Catenary session.
**Fields consumed from hook JSON:**
| `transcript_path` | Path to the Claude Code transcript file |
| `cwd` | Identifying which Catenary session to update |
### `catenary lock acquire`
Triggered before file edits (Claude Code `PreToolUse`, Gemini `BeforeTool`). Acquires a file-level
advisory lock, blocking until the lock is available or the timeout expires. This
serializes concurrent edits to the same file across multiple agents.
**Fields consumed from hook JSON:**
| `session_id` | Lock owner identity (primary key) |
| `agent_id` | Lock owner identity (appended if present) |
| `tool_input.file_path` or `tool_input.file` | File to lock |
| `cwd` | Resolving relative file paths and finding the session for monitor events |
**Flags:**
| `--timeout` | 180 | Seconds to wait before giving up |
| `--format` | plain | Output format (`plain` or `gemini`) |
**Output:** silent on success. On timeout, returns JSON with
`permissionDecision: "deny"`. If the file was modified since the owner's last
read, returns JSON with `additionalContext` warning.
### `catenary lock release`
Triggered after file edits (Claude Code `PostToolUse`, Gemini `AfterTool`).
Releases the lock with a grace period, allowing the same agent to re-acquire
without contention during the diagnostics→fix cycle.
**Fields consumed from hook JSON:**
| `session_id` | Lock owner identity |
| `agent_id` | Lock owner identity (appended if present) |
| `tool_input.file_path` or `tool_input.file` | File to unlock |
| `cwd` | Finding the session for monitor events |
**Flags:**
| `--grace` | 30 | Seconds before the lock expires |
| `--format` | plain | Output format (`plain` or `gemini`) |
### `catenary lock track-read`
Triggered after file reads (Claude Code `PostToolUse` on `Read`, Gemini
`AfterTool` on `read_file`). Records the file's modification time so future
lock acquisitions can detect if the file changed since the agent last read it.
**Fields consumed from hook JSON:**
| `session_id` | Owner identity for tracking |
| `agent_id` | Owner identity (appended if present) |
| `tool_input.file_path` or `tool_input.file` | File to track |
## Version Management
Three files carry the version number:
| `Cargo.toml` | `version` |
| `.claude-plugin/marketplace.json` | `plugins[0].version` |
| `gemini-extension.json` | `version` |
The `make release-*` targets bump all three atomically. A `version_sync` test
(`tests/version_sync.rs`) verifies they stay in sync.