Generated at build time from docs/contributing/writing-a-plugin.md
Writing a capability plugin
agemon’s work is split into capabilities (crg, daemon, skills, agents,
cli-tool, master-prompt). Each one is a plugin object that implements AgemonPlugin
from src/plugins/types.ts.
Plugins are built in. agemon does not load plugins from other packages; a new capability is a pull request to this repository. Open an issue first (see GOVERNANCE.md): a capability changes what agemon does on every user’s machine.
Where a plugin runs
Section titled “Where a plugin runs”A normal agemon run goes through these steps (src/core/orchestrator.ts,
src/core/consent.ts, src/core/apply.ts):
- Plan. Plugins run in dependency order (
dependsOn). Eachplan(ctx, plannedSoFar)returns theProposedOperations it wants: one per resource it would create, change, install or register. It must not change anything.plannedSoFarholds the operations of the plugins that planned before it. - Consent. Operations are grouped into consent gates (see “Operations” below), and
the user approves or declines each gate.
--yesapproves the gates that allow it; a non-interactive run without--yeswrites the plan and stops. - Stage. For approved operations,
materialize(ctx, operations)returns the files the plugin will write (StagedFile: target path, contents, kind). agemon writes them under.agemon/plans/<plan id>/staged/and validates them before anything is touched. - Apply. agemon snapshots every target file, writes a journal, then for each capability
with approved operations calls
apply(ctx, operations, staged), or, when the plugin has noapply,detectand theninstallif it is not present. Then it callsverify(ctx). A thrown error orverifyreturningok: falserolls back the whole run.applymay return{ skipped: [{ operationId, reason }] }for approved operations it deliberately left alone; those are reported as skipped and not counted as applied. Anapplythat never skips is declaredPromise<undefined>(notPromise<void>), so it matches the interface’s return type. - Rollback. Snapshots are restored, and for ledger entries this run added,
revert(ctx, entries)is called on the plugin that owns them. - Nuke.
agemon nukepreviews and then callsuninstall(ctx)on every capability with ledger entries, keeping a capability while a capability that depends on it is still installed.
agemon status and agemon inspect call describeState; verify also runs after an apply
to check the whole result.
The interface
Section titled “The interface”As defined in src/plugins/types.ts:
| Member | Required | What it does |
|---|---|---|
id |
yes | The capability id, used in agemon.toml, --only and the ledger. |
dependsOn |
no | Ids of capabilities that must plan and apply first. --only adds them. |
riskClass |
no | inert, writes-config, executes or credential-adjacent; describes the plugin as a whole. Each operation carries its own riskClass. |
sharedResources |
no | Machine-wide tools the plugin installs once for every repository (see src/core/machine-ledger.ts). |
detect(ctx) |
yes | Returns { present, preExisting }: is the capability already there, and was it there before agemon? |
desiredRevision(ctx, resourceId) |
no | The version or revision agemon wants for a resource; stored on its ledger entry. |
describeState(ctx) |
no | Rows for status/inspect: one per resource, with a CapabilityState. |
plan(ctx, plannedSoFar?) |
no | Proposed operations (step 1). Without it, the plugin is never part of a plan. |
materialize(ctx, operations) |
no | Staged files for approved operations (step 3). |
apply(ctx, operations, staged?) |
no | Makes the approved changes (step 4). May return { skipped } (step 4). |
install(ctx) |
no | Used by apply when the plugin has no apply. A plugin with plan and apply must not expose an install that plans and applies by itself: that would skip consent. Every plugin needs apply or install. |
verify(ctx) |
yes | Returns { ok, detail?, attention? }. attention is shown and makes status exit 2. |
revert(ctx, entries) |
no | Undoes the given ledger entries during a rollback (step 5). |
describeKeptAfterRollback(ctx, operations) |
no | After a rollback or agemon recover, names the changes this plugin deliberately did not undo (the skills plugin’s forward-only updates), e.g. updated skill x (agemon's newer version). agemon shows each as “Kept …” and lists “keep …” in the recover preview. |
uninstall(ctx) |
yes | Removes what the ledger says this plugin installed (step 6). |
Operations
Section titled “Operations”Build operations with describeOperation from src/plugins/proposed-operation.ts.
gateForOperation in src/core/consent.ts picks the consent gate, in this order:
conflict→ a decision the user must make (never answered by--yes);- a
resourceIdstarting withagent-file:→ “install subagents”; one starting withcustom-skill-group:, forinstall-packageandskip→ “install custom skills” (these prefixes belong toagentsandskills); skipwithrequiresConsent: true→ “install tooling”; any otherskip, anduse-shared, need no consent;install-package→ “install tooling”,register-service→ “register service”,merge-key→ “write config”,create/merge-block/adopt/replace→ “touch instruction files”.
An operation that reaches the action-based gates with riskClass: "inert" and
requiresConsent: false is refused: every change needs consent. riskClass does not choose
the gate; a credential-adjacent operation makes its gate always show the full diff, as the
“install custom skills” gate always does. preview is what
the user sees in the gate: a diff for file changes, a one-line note otherwise.
Rules every plugin follows
Section titled “Rules every plugin follows”- Record everything you change. Every file written, package installed or service
registered gets a ledger entry:
ctx.manifest.recordAction({ plugin, type, target, preExisting, … }).uninstallandrevertwork only from these entries; never from a guess about what is on disk. SetpreExisting: truefor anything that was there before agemon, sonukeleaves it alone. - Plan has no side effects.
plan,detect,describeStateandverifyonly read. - Honor
ctx.dryRun. In a dry run,apply/install/uninstalldescribe what they would do and change nothing. - Run commands through
ctx.run(command, args, options). Arguments are an array, never a shell string.ctx.runapplies timeouts and output limits and letsnuke’s preview refuse commands, souninstallmust not callchild_processdirectly. - Write files atomically and inside the repository (see
src/core/files.ts), unless the resource is a machine-wide shared tool. - Fail loudly. Throw with a message that says what failed and what the user can do. A
throw during apply triggers a rollback of the whole run;
revertmust be safe to call on entries whose change only partly happened. - Treat repository content as untrusted. Files,
agemon.tomland ledger entries can be edited by anyone with commit access; validate paths and names before you use them (seesrc/core/untrusted-text.ts).
A minimal plugin
Section titled “A minimal plugin”src/plugins/test-plugin.ts is the smallest valid plugin. It is not registered, so it never
takes part in a run; tests that need it add it themselves. It shows the required members and how
a plugin records and checks a ledger entry:
import type { AgemonPlugin } from "./types.js";
export const testPlugin: AgemonPlugin = { id: "test-plugin", riskClass: "inert", async detect(ctx) { return { present: ctx.manifest.hasActionForPlugin("test-plugin"), preExisting: false, }; }, async plan() { return []; }, async install(ctx) { if (ctx.dryRun) { return; } await ctx.manifest.recordAction({ plugin: "test-plugin", type: "simulated-install", target: "phase-1 orchestrator checkpoint", preExisting: false, }); }, async verify(ctx) { if (ctx.dryRun) { return { ok: true, detail: "dry-run" }; } return { ok: ctx.manifest.hasActionForPlugin("test-plugin"), detail: "action logged in manifest", }; }, async uninstall() {},};A real capability returns operations from plan. For a complete example of plan →
materialize → apply → revert for files, read src/plugins/agents/index.ts; for installing
a tool, src/plugins/cli-tool/index.ts.
Registering it
Section titled “Registering it”- Add the plugin to
capabilityRegistryinsrc/plugins/index.ts. The order there is the default order;dependsOnoverrides it. - A repository whose
agemon.tomllistscapabilitiesexplicitly never gets a new id on its own. If existing repositories should get the capability, add its id toAUTO_ADDED_TO_EXISTING_CONFIGSinsrc/core/capability-migration.ts(theagentscapability is the example). - Document it in the README (“What It Does”) and in
docs/architecture-and-operations.md.
Add test/unit/plugins/<id>.test.ts, modelled on the existing plugin tests: they build a
Context with fake commands (src/core/fake-subprocess.ts, used by
test/unit/plugins/cli-tool.test.ts) and a fake service manager
(test/unit/plugins/agents.test.ts), so nothing touches the real machine. Cover at least:
planon a clean repository and on one where the capability is already present;applyfollowed byverify;- a failure in the middle of
applythat leaves nothing behind after rollback (revert); uninstallafterapply, anduninstallwithpreExistingentries that must be kept.
Then run the full check from CONTRIBUTING.md, including the sandbox roundtrip, which applies and nukes in an isolated copy:
npm run sandbox -- roundtrip clean-repo