NAVIGATION
ELOWEN DOCUMENTATION

Last updated: 10 October 2026

Browse documentation · Plugin lifecycle and layout
Developer reference

Plugin lifecycle and layout

Lifecycle and layout

The loader scans the bundled plugin root first and the writable instance plugin root second (src/daemon/brainCore.ts:315). A name is loaded from the first root whose copy loads successfully: a copy that fails in an earlier root is logged and does not block a later root (src/plugins/loader.ts:337, 474). Only names in the enabled-plugin configuration are loaded. Within each root, plugin folders are processed in ascending name order, so registration order does not depend on the filesystem (src/plugins/loader.ts:332-335).

Each plugin is loaded into a staging registry. The loader parses and validates the manifest, verifies that the entry remains inside the plugin directory by a lexical path check that does not resolve symlinks (src/plugins/manifest.ts:42-47), imports the ESM entry, and calls register(ctx). Contributions are merged only after registration completes. A malformed manifest, failed import, or throwing registration skips that plugin without leaving partial tools or routes in the live registry.

Every daemon start builds the registry generation from disk, and a plugin change is applied by restarting, never by swapping the plugin in place (requestReload). The one exception is a plugin's skill set: requestReload({ mode: 'reload', skills }) replaces it in the running daemon, because skills are data that every reader resolves per turn. Do not cache a control, configuration object, or other live registry value; resolve sibling controls at the time of use.

A minimal plugin has this shape:

my-plugin/
├── elowen-plugin.json
├── index.mjs
├── icon.svg
├── i18n/
│   └── cs.json
├── prompt/
├── web-src/
└── web/

The icon, translation files, prompt fragments, browser sources, and browser bundle are optional. The entry path is relative to the plugin directory.

Server-side plugin code should use the stable plugin API package and the published packages it declares. Do not import Elowen application internals. Browser bundles must never import from Elowen's web/ application. They use the browser runtime described below.

A user-root plugin also has to be in the licence set. The loader refuses a user-root plugin outside that set with not included in this licence before its entry is imported, and the failure takes the normal skip path. The bundled root is never gated (src/plugins/loader.ts:354-359). The set is built as loadablePlugins in src/daemon/brainCore.ts:373.

Each import URL carries the manifest version and the entry file's modification time. The loader's own comment gives the reason: Node caches a module by URL for the life of the process, so a later load must not reuse a stale module after an in-place update (src/plugins/loader.ts:360-364).

A skipped plugin is logged as plugin skipped: <name>: <reason> with code plugin.load_failed, and the load alert is synced (src/plugins/loader.ts:477-480). A folder without elowen-plugin.json is not a plugin and is skipped without a message (src/plugins/loader.ts:347). An enabled name that no root provides is reported once at the end as plugin.enabled_missing (src/plugins/loader.ts:488-490).

Owning code and minimal use

The loader is loadPlugins in src/plugins/loader.ts, which stages, validates and merges each plugin. discoverPlugins in the same file lists folders by parsing manifests without importing any code, so a request handler may call it. The manifest checks are parseManifest and validateManifestFolder in src/plugins/manifest.ts. The roots and the licence set are wired in src/daemon/brainCore.ts. The public surface is PluginContext and PluginChange in src/plugins/api.ts.

The one live way to change a skill set without a restart is requestReload with the complete new set:

await ctx.requestReload({ mode: 'reload', skills: nextSkills });

Any other change uses the restart form, which carries a reason:

await ctx.requestReload({ mode: 'restart', reason: 'settings changed' });

The method takes the plugin from its context, so a plugin can only replace its own skills. When it rejects, the plugin's write already happened, so the plugin should report the change as saved but not applied rather than as a success (src/plugins/api.ts:2402-2415). The real consumer is the skills plugin in the registry, which calls requestReload({ mode: 'reload', skills: collectSkills() }) after it changes its skill files.

Discovery is cheap and safe to repeat, because it parses manifests only (src/plugins/loader.ts:107-108). A plugin change of any kind other than a skill set costs one daemon restart, which pauses running turns first.