Skip to content

Plugin Model

github-actions[bot] edited this page Oct 2, 2026 · 2 revisions

Plugin Model

Note

Contributor documentation. Describes the plugin object model, not how to configure it — see Configuration Reference for that.

How the objects a plugin repository declares are shaped and how they reference each other — the domain model of the configuration vocabulary itself (plugin.yaml and trunk.yaml), independent of any component that reads it. This is the schema a redesign's config resolver has to keep faithfully representable; see Architecture.md for the components that consume it and Plugin-Sources.md for the resolution/fetch architecture built on top.

The field survey below is based on the trunk-io/plugins catalog at commit d1e3af5752059371e206fe8f242b15d7f4205555 (208 plugin.yaml files).

Plugin repositories, sources, and the catalog

A plugin repository is a directory tree with one subdirectory per category — linters, tools, runtimes, actions — each holding one file per resource, plus category-root and repository-root files that carry declarations with no enabled/disabled id of their own:

  • The repository root file declares repository-wide metadata (a minimum required tool version) and global environment tables (environments:, a list of named environment-entry groups) — always in effect, not selected by anything in trunk.yaml.
  • A category root file (for example, the linters category's own root) declares that category's shared vocabulary: named comment formats (the leading/trailing delimiter pairs used to recognize inline ignore directives) and the shared file-type registry entries every linter's files: list references by name.
  • Every other file contributes resource definitions: one or more linters, tools, runtimes, or actions, plus any download recipes those definitions need.

trunk.yaml's plugins.sources list names one or more plugin repositories to pull from — a local path, or a remote git repository pinned to a tag or commit. Every source's contributed definitions merge into one shared, id-keyed catalog per category. trunk.yaml never declares a linter, tool, runtime, or action itself — it only enables an id a source's catalog already declares, via lint.enabled / runtimes.enabled / actions.enabled (+ actions.disabled), each entry optionally pinning a version with id@version. Enabling id@version changes which version is resolved for that id's own downloads; it does not choose among that id's several command variants (see "Commands" below) — nothing in trunk.yaml does.

Merge/override: two sources declaring the same id in the same category is a same-namespace collision — the catalog is flat and id-keyed per category, so the later-merged source's definition replaces the earlier one for that id. A real, current example of an intentional same-linter collision within one source's own catalog (not across sources) is a deprecated alias id: an older linter id kept only so an existing trunk.yaml that still enables it keeps resolving to something, carrying a human-readable deprecated: migration message pointing at the id that superseded it (for example, a bare formatter id superseded by that same tool's fuller, combined lint+format linter id).

The class diagram

classDiagram
    class PluginRepository {
        root metadata (min. required version)
        global environment tables
        category-root vocabulary (comment formats, file types)
    }
    class TrunkYAML {
        plugins.sources[]
        lint.enabled[] "id@version"
        runtimes.enabled[] "id@version"
        actions.enabled[] / actions.disabled[]
    }
    class DownloadRecipe {
        name
        per OS/CPU download entries (each optionally version-ranged)
        derived template args (regex-extracted from ${version})
    }
    class ToolDefinition {
        name
        exposed shim names
        known-good/pinned version
        health checks (run after install)
    }
    class RuntimeDefinition {
        kind (go, node, python, php, rust, ruby, ...)
        shim names
        runtime-environment table
        linter-environment table (added only for tools depending on this runtime)
    }
    class LinterDefinition {
        name
        matched file types
        tool references
        one or more CommandDefinitions
        config-file-presence triggers (direct_configs)
        cache-invalidating file triggers (affects_cache)
    }
    class CommandDefinition {
        invocation template
        output format (+ optional output parser)
        formatter flag
        in-place flag
        batch flag
        run-from location
        supported version range
        platform restriction
        fix prompt / fix verb (quick-fix UX text)
        security-category flag
        supersedes-upstream flag
    }
    class OutputParser {
        runtime kind
        conversion script template
    }
    class ActionDefinition {
        id
        runtime kind (optional)
        invocation template
        one or more Triggers
        interactivity requirement
        notify-on-error flag
    }
    class Trigger {
        git hook names, or
        file-change globs, or
        periodic schedule
    }
    class FileTypeDefinition {
        name
        extensions / filenames / regexes / shebangs
        required YAML keys (disambiguates a YAML dialect)
        comment formats it uses
        inherited file types
    }
    class EnabledEntry["Enabled Entry (trunk.yaml)"] {
        id
        pinned version (optional)
    }

    PluginRepository "1" --> "*" LinterDefinition
    PluginRepository "1" --> "*" ToolDefinition
    PluginRepository "1" --> "*" RuntimeDefinition
    PluginRepository "1" --> "*" ActionDefinition
    PluginRepository "1" --> "*" DownloadRecipe
    PluginRepository "1" --> "*" FileTypeDefinition

    TrunkYAML "1" --> "*" PluginRepository : plugins.sources
    TrunkYAML "1" --> "*" EnabledEntry
    EnabledEntry "*" --> "1" LinterDefinition : selects by id (or Runtime/Action)

    LinterDefinition "1" --> "*" CommandDefinition
    LinterDefinition "*" --> "*" ToolDefinition : invokes, by name
    LinterDefinition "*" --> "*" FileTypeDefinition : matches, by name
    LinterDefinition "0..1" --> "1" DownloadRecipe : inline tool binding (shorthand, no separate ToolDefinition)
    LinterDefinition "0..1" --> "1" RuntimeDefinition : inline tool binding (shorthand)
    CommandDefinition "0..1" --> "1" OutputParser
    ToolDefinition "0..1" --> "1" RuntimeDefinition : runs under, by name (XOR with Download)
    ToolDefinition "0..1" --> "1" DownloadRecipe : fetched via, by name (XOR with Runtime)
    RuntimeDefinition "0..1" --> "1" DownloadRecipe : fetched via, or "expect present on host"
    ActionDefinition "1" --> "*" Trigger
    ActionDefinition "0..1" --> "1" RuntimeDefinition : runs under
    FileTypeDefinition "*" --> "*" FileTypeDefinition : inherits
Loading

The inline tool-binding shorthand

A linter definition normally names one or more tools by id (tools: [...]), each a separate ToolDefinition naming a runtime+package or a download recipe. A common shorthand in the real catalog skips that indirection: the linter definition itself carries a download: and/or runtime:/package: field directly, binding to a download recipe or runtime without a ToolDefinition in between at all. Both forms coexist across the catalog for different linters — neither supersedes the other — so a redesign's object model has to represent a linter's tool binding as either a reference to one or more named ToolDefinitions or an inline binding of the same shape a ToolDefinition would otherwise carry, not just the reference form.

The legacy single-command shape

A small number of real, currently-declared linter ids (all carrying a deprecated: migration message pointing at their replacement) use an older shape that predates the commands: [...] list: a single type/command pair directly on the linter definition instead of one or more CommandDefinition entries. This shape is not merely historical — it is live in the current catalog on deprecated-but-still-enableable ids, so an existing trunk.yaml that still enables one of them is a real input a redesign's model has to account for, either by modeling the shape or by treating it as a resolvable-to-empty legacy id with a surfaced warning. See Decision-Log.md for what happens today when neither is done.

Field-category table (conceptual, not exhaustive)

Object Identity Execution Output Gating Environment
LinterDefinition name tool references, run-from context — matched file types, direct_configs (config-presence trigger), suggest_if (when to recommend enabling) linter-scoped environment entries
CommandDefinition name invocation template, batch flag, sandbox mode, target shape output format, output parser, formatter/in-place flags version range, platform restriction, enabled flag, success/error exit codes —
ToolDefinition name exposed shim names — known-good/pinned version health checks (run after install)
RuntimeDefinition kind package-install mechanism (by kind) — known-good version, "expect present on host" runtime-environment + linter-environment tables
ActionDefinition id invocation template, runtime reference — triggers (hook/file-change/schedule), interactivity requirement action-scoped environment entries
DownloadRecipe name — per-OS/CPU URL templates per-entry version range —
FileTypeDefinition name — — extension/filename/regex/shebang/required-key matchers, inheritance —

Declared fields and whether the execution engine acts on them

The catalog survey below (counts are occurrences across the full real plugin catalog, not just distinct ids) groups every field found on CommandDefinition/LinterDefinition/ToolDefinition/ ActionDefinition that a redesign's object model should represent, with its status today: the v0.11 catalog-fidelity work made the engine act on several of them, while the rest are parsed and never consulted, or never parsed at all because the corresponding struct has no field for it. Full detail, evidence, and severity for each is in Decision-Log.md; this is the field-level index into that.

Field Declared on Catalog occurrences Status today
suggest_if linter 129 consulted by linters list (v0.11)
run_timeout linter 8 enforced on the linter's commands (v0.11)
cache_results linter 4 parsed, never consulted
cache_results command 71 not modeled (dropped while parsing)
supported_platforms linter 9 parsed, never consulted
platforms command 15 modeled, consulted (v0.10)
version (supported range) command 20 parsed, consulted (v0.10)
is_security command 22 modeled; tags findings, --security-only filters (v0.11)
disable_upstream command 13 not modeled (dropped while parsing)
fix_prompt / fix_verb command 3 / 3 parsed, carried through resolution (v0.10); no consumer yet
prepare_run command 2 modeled; run once before the first invocation (v0.11)
stdin command 7 not modeled (dropped while parsing); its real instances all pair with formatter: true/no in_place, a shape now run unconditionally piping stdin (v0.10, entry 5) regardless of this field's own value
max_concurrency command 4 modeled; caps parallel invocations per command (v0.11)
health_checks tool 20 modeled; run after install (v0.11)
extra_packages tool 6 modeled; installed alongside the tool's package (v0.11)
output_type action 2 not modeled (dropped while parsing)

Where to go next

Clone this wiki locally