Plugin System
Every capability Merlin has — file edits, search, shell, git, specs, memory, AlgoChat — is a fledge plugin. There’s no in-tree shortcut. Plugins are subprocesses; communication is JSON-lines over stdin/stdout via the fledge-v1 protocol.
End-to-end Flow
LLM produces ToolUse block
│
│ {"name": "files-write", "input": {"path": "...", "content": "..."}}
▼
Agent::execute_tool_calls
│
│ looks up CommandSpec by name
│ translates structured input → positional argv via spec
▼
plugin::run_plugin_command("files-write", &["src/foo.rs", "hi"])
│
│ Command::new("fledge").args(["plugins", "run", name, "--ni", argv...])
▼
fledge plugins run files-write --ni src/foo.rs hi
│
▼
fledge spawns the plugin binary
│
│ sends InitMessage on stdin (JSON-lines)
▼
plugin reads InitMessage { command, args, project, plugin, fledge }
│
│ runs its logic
│ writes OutboundMessage::Output{...} on stdout
▼
fledge captures stdout, returns to Merlin as PluginResult
│
▼
Agent emits AgentEvent::ToolResult { name, success }
│
│ tracks file_changed if name is files-write or files-edit
▼
ContentBlock::ToolResult fed back into context for next LLM turn
Discovery
plugin::discover_plugins() shells to fledge plugins list --json and
parses the registry. Each entry has:
| Field | Description |
|---|---|
name | Plugin name, e.g. "fledge-plugin-files" |
commands | List of command names exposed |
version | From the plugin’s manifest |
source | Source repository identifier |
trust_tier | "official", "community", etc. |
runtime | "native", "wasm", etc. |
Per-command Schemas
Once plugins are discovered, plugin::load_command_specs(&plugins)
reads each plugin’s plugin.toml from the fledge data directory and
extracts per-command argument declarations:
[[commands]]
name = "files-edit"
description = "Replace the first occurrence of a string in a file"
binary = "target/release/fledge-plugin-files"
args = [
{ name = "path", type = "string", required = true,
description = "Path to the file to modify" },
{ name = "old", type = "string", required = true,
description = "Exact substring to replace" },
{ name = "new", type = "string", required = true,
description = "Replacement string" },
]
These become CommandSpecs, which power two transforms:
build_input_schema(spec)— generates a JSON Schema for the LLM’s tool-call input. Anthropic, OpenAI, and Ollama all consume structuredinput_schema.translate_input_to_argv(spec, input)— maps the LLM’s structured response back to positional CLI args in declaration order. Required-but-missing args fail loudly.
For plugins without a readable manifest (e.g. external plugins like
fledge-plugin-memory not in our repo), CommandSpec::legacy_fallback
produces a single args: string schema and the agent splits on
whitespace before invoking. This preserves backwards compat with
plugins that haven’t migrated to typed args.
Why Subprocess?
Subprocess isolation gives:
- Language flexibility — plugins can be Rust, Go, Python, anything that can read JSON-lines.
- Crash isolation — a panicking plugin doesn’t take down the agent.
- Trust gating —
trust_tieranddangerous = trueflags surface to the CLI before running risky tools (currently advisory). - Auditability — fledge logs every command invocation.
The cost is per-call overhead (process spawn + JSON parse). For the kinds of operations Merlin makes (file I/O, git, grep), this is dwarfed by I/O latency.
Internal Plugins
A broad set of plugins ships in plugins/. The core set:
| Plugin | Commands |
|---|---|
fledge-plugin-files | files-read, files-write, files-edit, files-glob, files-list, files-mkdir, files-stat |
fledge-plugin-search | search-grep, search-references, search-symbols |
fledge-plugin-shell | shell-exec (dangerous) |
fledge-plugin-specsync | specsync-check, specsync-list, specsync-read, specsync-create |
fledge-plugin-git | git-status, git-diff, git-commit, git-branch, git-add, git-checkout, git-stash, git-log |
fledge-plugin-cargo | cargo-build, cargo-test, cargo-clippy, and more |
fledge-plugin-node | node-run, auto-detects npm/bun/pnpm/yarn |
fledge-plugin-vision | Image description via local Ollama |
fledge-plugin-voice | Transcription (Whisper) and synthesis (OpenAI tts-1) |
fledge-plugin-discord-bridge | Discord bot bridge |
fledge-plugin-telegram-bridge | Telegram bot bridge |
fledge-plugin-merlin-subagent | In-loop sub-agent delegation (subagent-spawn) |
fledge-plugin-memory-merlin | Agent memory persistence |
fledge-plugin-web | Web fetch |
fledge-plugin-pwd-info | Working directory info |
Each is a single binary built from plugins/<name>/, with
plugin.toml declaring commands and arg schemas, and a src/main.rs
that reads InitMessage and dispatches by command name.
See Plugin Authoring to write your own.