Discovery, installation and reload
Theo can now distribute a working folder, but another maintainer cannot find the tool. The obstacle is not TypeScript: it is discovery scope.
He separates four questions:
- Is the package installed or linked?
- Is its entry selected by discovery?
- Did the factory bind successfully?
- Is the tool enabled and presented in the expected way?
Creation and loading formats
| Format | Exact entry contract | Typical use |
|---|---|---|
Single .ts or .js file | Default factory | First extension or tightly focused integration |
| Async default factory | Returns Promise<void> | Read adjacent assets before registration |
Directory with index.ts / index.js | Default factory in the selected index | Entry plus ordinary helpers |
Directory with package.json | omp.extensions; legacy pi.extensions fallback | Several explicit entries |
Explicit .mjs / .cjs file | ESM default factory or compatible CommonJS factory export | JavaScript distribution without TypeScript |
| Installed plugin manifest | Base entries plus enabled feature entries | Managed distribution |
| SDK inline factory | Function passed to extensions | Host-owned composition |
| Prepared factory rebinding | Imported factory called against a fresh API/runtime | Avoid repeated module evaluation without sharing bound state |
For normal ESM modules, an arbitrary named export is not enough.
Complete JavaScript exercise—save as format-note.mjs:
export default function formatNote(pi) {
pi.registerCommand("format-note", {
description: "Identify the explicitly loaded module format",
async handler(_args, ctx) {
ctx.ui.notify("Explicit ESM extension loaded.", "info");
},
});
}
Complete CommonJS exercise—alternative file format-note.cjs; do not load both alternatives together:
module.exports = function formatNote(pi) {
pi.registerCommand("format-note", {
description: "Identify the explicitly loaded module format",
async handler(_args, ctx) {
ctx.ui.notify("Explicit CommonJS extension loaded.", "info");
},
});
};
Terminal shell—choose one explicit file:
omp --no-extensions -e ./format-note.mjs
Scanning and manifest resolution are not identical
Native and configured directory scanning automatically discover direct .ts/.js files and immediate child entry packages. They do not recursively import arbitrary helpers.
Installed-plugin manifest expansion is broader: it supports .ts, .js, .mjs and .cjs, and corresponding index files.
There are additional implementation differences worth knowing:
- Native discovery uses globbing with gitignore/hidden filtering; explicit configured-directory scanning uses
readdirand does not apply that same filtering. - Native child manifests with declared entries suppress index fallback.
- Installed-plugin directory manifests are authoritative even when a declared entry is missing; a stale index is not substituted.
- The explicit-directory resolver falls back to an index when no declared entry survives its existence checks.
- Installed-plugin scanning excludes declaration files. Do not place
.d.tsfiles in loose native/configured scan directories and assume every scanner excludes them. - For portable manifests, list concrete entry files. Do not depend on every loader resolving directory-valued manifest entries in exactly the same way.
Where native extension factories come from
The ambient native project root is cwd-only:
<cwd>/.omp/extensions- the active agent directory’s
extensionsfolder
The default user agent directory is ~/.omp/agent. A named profile uses ~/.omp/profiles/<name>/agent; PI_CODING_AGENT_DIR can override it.
Native discovery also reads legacy settings.json extension lists. The main startup path adds merged extensions settings, normally from the active agent config.yml and project configuration.
Plain <project>/extensions is not the same as <project>/.omp/extensions. The former needs explicit/configured loading; the latter is a native ambient root.
Legacy pi package metadata and some .pi compatibility lookups remain supported. .pi/extensions is not a native ambient module root in this snapshot.
Skill ancestor traversal must not be used to infer extension-module traversal.
Ordered inputs and two layers of deduplication
The module discovery pipeline appends:
- native extension-module capability items;
- discovered JS/TS hook factories;
- enabled installed-plugin extension entries;
- explicit/configured paths.
Within the SDK’s configured lane, CLI additional paths precede settings paths.
There are two different identity operations:
- Capability discovery can deduplicate by derived extension name. Native project items precede user items.
- The final path list deduplicates normalized absolute paths, first seen wins.
The final path deduplication is not a universal realpath-equivalence guarantee. Two different symlink spellings are not necessarily one path-list identity.
Imports may be prepared concurrently, but factories are bound sequentially in path order. Do not rely on module-scope side effects occurring in factory order.
Explicit-only and trusted-file loading
--no-extensions means explicit-only for this extension-factory path and its OMP extension-package sibling roots. Explicit -e, --extension and --hook entries remain eligible.
It is not full isolation. Other tools, skills, rules, prompts and MCP discovery families have their own controls.
--trusted-extension is stricter about entry selection:
- it requires an existing absolute module file path;
- it rejects directories;
- it canonicalizes the selected file’s real path;
- it cannot be combined with
-e,--extensionor--hook; - a trusted entry load failure aborts that startup path rather than merely becoming an ordinary per-module warning.
Terminal shell—trusted-file selection using the actual absolute example path:
omp --trusted-extension "$EXAMPLES/package-lab/single/field-notes.ts"
Do not copy the relative trusted-path form from older example prose: the supplied current parseArgs() requires an absolute path.
Trusted-file selection does not:
- sandbox the factory;
- authenticate its dependencies;
- restrict ordinary imports;
- remove in-process filesystem or network authority;
- create a universal prohibition on unrelated discovery systems.
Disable an entry without deleting it
A derived ID looks like:
extension-module:field-notesforfield-notes.ts;extension-module:multiformulti/index.ts.
Config file—an example setting in the active agent’s config.yml:
disabledExtensions:
- extension-module:multi
In normal settings-aware discovery, this filters the directory-discovered multi/index.ts entry. An explicitly configured file remains an override in this build.
Observed: the disabled directory case selected zero entries; the explicit file case selected one.
Under disableExtensionDiscovery, the SDK does not use the normal ambient disabled-ID list for the explicit lane. Do not use --no-extensions to test whether normal ambient disabling works.
Other capability families use their own IDs in disabledExtensions. A hook’s disabled identity is not automatically its extension-module name.
Installation is an opt-in change to your environment
The workbook requires no plugin installation. Explicit -e is enough.
If you intentionally want to try native plugin management, these commands change the user plugin installation state. The scratch agent directory above is not a guarantee that plugin storage is project-local.
Terminal shell—optional developer installation:
omp plugin link "$EXAMPLES/package-lab/manifest"
omp plugin list --json
omp plugin features omp-workbook-field-notes
omp plugin features omp-workbook-field-notes --enable summary
A linked plugin starts with feature defaults. summary defaults off.
Launch normally, without --no-extensions, when checking ambient installed-plugin discovery.
omp plugin install ./local-directory routes to the same link operation. It is not a package copy into the current project.
Current installation boundaries:
| Operation | What it does |
|---|---|
plugin link <directory> | Symlinks into the user plugin node_modules tree and records runtime state |
plugin install <local-directory> | Routes to link |
| npm or git installation | Runs package-manager work and may fetch dependencies |
plugin install name@marketplace --scope project | Uses the distinct marketplace project-scope route |
plugin disable <name> | Disables an installed plugin without removing its source |
plugin enable <name> | Re-enables it |
plugin uninstall <name> | Removes installation/runtime registration through the appropriate route |
Adding --scope project does not make local/npm installation project-scoped. The local/npm install handler warns that scope is supported only for marketplace installs.
Runtime discovery can read both user and project plugin roots. Enabled project packages shadow user packages with the same package name. That does not imply every installer writes to the project root.
Features and settings
Install-spec feature syntax is:
package: default features;package[summary]: selected features;package[*]: all optional features;package[]: no optional features.
Quote bracketed shell arguments so your shell does not expand them. Use a real package spec; the workbook’s private teaching package is not claimed to exist in a registry.
Plugin feature metadata supports description, default, and additional extensions, tools, hooks and commands.
Plugin settings support:
- string, number, boolean and enum types;
- descriptions and defaults;
secretdisplay masking;- an
envfallback declaration; - numeric
min,max,step; - enum
values.
A settings declaration is not executable behavior. The supplied getPluginSettings() helpers merge saved global/project values; they do not inject a settings object into ExtensionAPI or magically apply all defaults/environment fallbacks to extension code. A consumer must resolve and validate its settings deliberately.
The CLI supports plugin config list|get|set|delete|validate. In this snapshot, the parsed --local flag is not forwarded into the manager’s setter: do not advertise it as a reliable project-local write route.
Project overrides have their own file:
Config-file exercise—.omp/plugin-overrides.json, selecting the real example feature:
{
"features": {
"omp-workbook-field-notes": ["summary"]
}
}
The same override structure supports disabled package names and per-package settings.
secret: true is not secure credential storage. Human CLI display may mask a value while JSON output still exposes it. Keep real credentials out of source, screenshots, transcript details and published config.
Collisions have family-specific rules
| Registration family | Current behavior |
|---|---|
| Same tool/command name inside one factory | Later Map entry replaces earlier |
| Tools across extension instances | Registrations are retained; effective lookup is later-extension-wins |
| Commands across extension instances | Effective command lookup is later-extension-wins |
| Aggregated built-in command collision | Filtered with diagnostics |
| Event handlers | Append and run in registration/load order, subject to event-specific short-circuiting |
| Custom message renderer lookup | First extension with that type wins |
| Assistant thinking renderers | All returned components append in registration order |
| Flags and composer-shape IDs | Aggregate last-wins |
| Nonreserved shortcut collision | Warning; later normalized key wins |
| Reserved shortcut | Rejected from the effective shortcut set |
Do not load all Seed Desk stages together and infer which version won from one notification.
Registered CLI flags can shadow same-named built-ins in the extension-aware parser. Prefer namespaced flags so startup behavior is not surprising.
The current CLI validates requested tool names against the fully discovered session registry. Older custom-tool documentation saying only built-in names are validated is stale. Still, --tools/--no-tools is not a universal isolation switch: unrestricted extension/custom tools have their own inclusion behavior.
Reload means what the host actually reloads
This snapshot contains a particularly important distinction:
AgentSession.reload()reopens the current session file throughswitchSession().- The supplied TUI command-context
reload()calls that session reload and rebuilds presentation. - RPC/ACP plugin-refresh code clears caches and refreshes skills/file-command metadata, but does not itself reimport every extension factory.
- The module loader supports cache-busted imports when a host actually invokes it again.
Therefore:
For editing extension code, restarting the explicit launch is the reliable workbook procedure. Do not promise that
ctx.reload()hot-rebinds the factory.
Treat await ctx.reload(); return; as the end of the current command handler. Factory-local notebook state need not reset on a session reload.
The loader’s same-process source cache-busting also has platform-specific behavior. In particular, the supplied compatibility loader notes a Windows file:// query limitation. A fresh process avoids treating an unverified hot-reload path as proof.
Failure recovery and removal
An ordinary invalid export, import failure or factory exception becomes a path-specific loader error; later modules still bind.
Factory failure restores the queued provider-registration list, including earlier registrations the failed factory removed. It is not a transaction over arbitrary side effects. Recorded checks showed a failed factory’s flag default surviving that rollback.
Inline loadExtensionFromFactory() rejects its promise instead of returning a per-path loader error record.
For recovery:
- Restart with only one explicit reviewed entry.
- Read the named load error.
- Check adjacent assets and dependencies.
- Restore a known-good source version.
- Restart and inspect tool/command metadata before invoking a mutation.
For removal:
- remove the explicit/configured path, or move the authored entry outside discovery;
- disable/uninstall an installed plugin through its installation route;
- restart and verify that no second path still loads it;
- keep session files unless you intentionally want to discard their data.
Terminal shell—optional installed-plugin lifecycle checks:
omp plugin disable omp-workbook-field-notes
omp plugin enable omp-workbook-field-notes
omp plugin uninstall omp-workbook-field-notes
omp plugin list --json
Inspect the result. A linked source directory remains your source; uninstalling is not a request to delete that checkout.
Exercise: an extension remains active after disabling its plugin. What is the first likely duplicate route to inspect?
Answer: an explicit -e or extensions: path, or a loose copy in a native extension directory.
Source, snapshot 2026-08-29: packages/coding-agent/src/extensibility/extensions/loader.ts; packages/coding-agent/src/discovery/builtin.ts, helpers.ts; packages/coding-agent/src/extensibility/plugins/loader.ts, manager.ts, types.ts; packages/coding-agent/src/cli/args.ts, plugin-cli.ts; packages/coding-agent/src/main.ts, buildSessionOptions; packages/coding-agent/src/session/agent-session.ts, reload.
Extensions inside those boundaries · Source chapter: extensions/discovery-installation-and-reload. Original evidence remains scoped to its recorded snapshot.