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:

FieldDescription
namePlugin name, e.g. "fledge-plugin-files"
commandsList of command names exposed
versionFrom the plugin’s manifest
sourceSource 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:

  1. build_input_schema(spec) — generates a JSON Schema for the LLM’s tool-call input. Anthropic, OpenAI, and Ollama all consume structured input_schema.
  2. 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 gatingtrust_tier and dangerous = true flags 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:

PluginCommands
fledge-plugin-filesfiles-read, files-write, files-edit, files-glob, files-list, files-mkdir, files-stat
fledge-plugin-searchsearch-grep, search-references, search-symbols
fledge-plugin-shellshell-exec (dangerous)
fledge-plugin-specsyncspecsync-check, specsync-list, specsync-read, specsync-create
fledge-plugin-gitgit-status, git-diff, git-commit, git-branch, git-add, git-checkout, git-stash, git-log
fledge-plugin-cargocargo-build, cargo-test, cargo-clippy, and more
fledge-plugin-nodenode-run, auto-detects npm/bun/pnpm/yarn
fledge-plugin-visionImage description via local Ollama
fledge-plugin-voiceTranscription (Whisper) and synthesis (OpenAI tts-1)
fledge-plugin-discord-bridgeDiscord bot bridge
fledge-plugin-telegram-bridgeTelegram bot bridge
fledge-plugin-merlin-subagentIn-loop sub-agent delegation (subagent-spawn)
fledge-plugin-memory-merlinAgent memory persistence
fledge-plugin-webWeb fetch
fledge-plugin-pwd-infoWorking 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.