## Discovery\,&#32;installation and reload

Theo can now distribute a working folder\,&#32;but another maintainer cannot find the tool\.&#32;The obstacle is not TypeScript\:&#32;it is discovery scope\.

He separates four questions\:

1. Is the package installed or linked\?
2. Is its entry selected by discovery\?
3. Did the factory bind successfully\?
4. Is the tool enabled and presented in the expected way\?

### Creation and loading formats

| Format | Exact entry contract | Typical use |
| --- | --- | --- |
| Single&#32;`.ts`&#32;or&#32;`.js`&#32;file | Default factory | First extension or tightly focused integration |
| Async default factory | Returns&#32;`Promise<void>` | Read adjacent assets before registration |
| Directory with&#32;`index.ts`&#32;\/&#32;`index.js` | Default factory in the selected index | Entry plus ordinary helpers |
| Directory with&#32;`package.json` | `omp.extensions`\;&#32;legacy&#32;`pi.extensions`&#32;fallback | Several explicit entries |
| Explicit&#32;`.mjs`&#32;\/&#32;`.cjs`&#32;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&#32;`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\,&#32;an arbitrary named export is not enough\.

**Complete JavaScript exercise—save as&#32;`format-note.mjs`\:**

~~~js
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&#32;`format-note.cjs`\;&#32;do not load both alternatives together\:**

~~~js
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\:**

~~~sh
omp --no-extensions -e ./format-note.mjs
~~~

### Scanning and manifest resolution are not identical

Native and configured directory scanning automatically discover direct&#32;`.ts`\/`.js`&#32;files and immediate child entry packages\.&#32;They do not recursively import arbitrary helpers\.

Installed\-plugin manifest expansion is broader\:&#32;it supports&#32;`.ts`\,&#32;`.js`\,&#32;`.mjs`&#32;and&#32;`.cjs`\,&#32;and corresponding index files\.

There are additional implementation differences worth knowing\:

- Native discovery uses globbing with gitignore\/hidden filtering\;&#32;explicit configured\-directory scanning uses&#32;`readdir`&#32;and 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\;&#32;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\.&#32;Do not place&#32;`.d.ts`&#32;files in loose native\/configured scan directories and assume every scanner excludes them\.
- For portable manifests\,&#32;list concrete entry files\.&#32;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&#32;**cwd\-only**\:

- `<cwd>/.omp/extensions`
- the active agent directory’s&#32;`extensions`&#32;folder

The default user agent directory is&#32;`~/.omp/agent`\.&#32;A named profile uses&#32;`~/.omp/profiles/<name>/agent`\;&#32;`PI_CODING_AGENT_DIR`&#32;can override it\.

Native discovery also reads legacy&#32;`settings.json`&#32;extension lists\.&#32;The main startup path adds merged&#32;`extensions`&#32;settings\,&#32;normally from the active agent&#32;`config.yml`&#32;and project configuration\.

Plain&#32;`<project>/extensions`&#32;is not the same as&#32;`<project>/.omp/extensions`\.&#32;The former needs explicit\/configured loading\;&#32;the latter is a native ambient root\.

Legacy&#32;`pi`&#32;package metadata and some&#32;`.pi`&#32;compatibility lookups remain supported\.&#32;`.pi/extensions`&#32;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\:

1. native extension\-module capability items\;
2. discovered JS\/TS hook factories\;
3. enabled installed\-plugin extension entries\;
4. explicit\/configured paths\.

Within the SDK’s configured lane\,&#32;CLI additional paths precede settings paths\.

There are two different identity operations\:

- Capability discovery can deduplicate by derived extension&#32;**name**\.&#32;Native project items precede user items\.
- The final path list deduplicates normalized absolute&#32;**paths**\,&#32;first seen wins\.

The final path deduplication is not a universal realpath\-equivalence guarantee\.&#32;Two different symlink spellings are not necessarily one path\-list identity\.

Imports may be prepared concurrently\,&#32;but factories are bound sequentially in path order\.&#32;Do not rely on module\-scope side effects occurring in factory order\.

### Explicit\-only and trusted\-file loading

`--no-extensions`&#32;means explicit\-only for this extension\-factory path and its OMP extension\-package sibling roots\.&#32;Explicit&#32;`-e`\,&#32;`--extension`&#32;and&#32;`--hook`&#32;entries remain eligible\.

It is not full isolation\.&#32;Other tools\,&#32;skills\,&#32;rules\,&#32;prompts and MCP discovery families have their own controls\.

`--trusted-extension`&#32;is stricter about&#32;**entry selection**\:

- it requires an existing&#32;**absolute module file path**\;
- it rejects directories\;
- it canonicalizes the selected file’s real path\;
- it cannot be combined with&#32;`-e`\,&#32;`--extension`&#32;or&#32;`--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\:**

~~~sh
omp --trusted-extension "$EXAMPLES/package-lab/single/field-notes.ts"
~~~

Do not copy the relative trusted\-path form from older example prose\:&#32;the supplied current&#32;`parseArgs()`&#32;requires an absolute path\.

Trusted\-file selection does&#32;**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-notes`&#32;for&#32;`field-notes.ts`\;
- `extension-module:multi`&#32;for&#32;`multi/index.ts`\.

**Config file—an example setting in the active agent’s&#32;`config.yml`\:**

~~~yaml
disabledExtensions:
  - extension-module:multi
~~~

In normal settings\-aware discovery\,&#32;this filters the directory\-discovered&#32;`multi/index.ts`&#32;entry\.&#32;An explicitly configured&#32;**file**&#32;remains an override in this build\.

**Observed\:**&#32;the disabled directory case selected zero entries\;&#32;the explicit file case selected one\.

Under&#32;`disableExtensionDiscovery`\,&#32;the SDK does not use the normal ambient disabled\-ID list for the explicit lane\.&#32;Do not use&#32;`--no-extensions`&#32;to test whether normal ambient disabling works\.

Other capability families use their own IDs in&#32;`disabledExtensions`\.&#32;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\.&#32;Explicit&#32;`-e`&#32;is enough\.

If you intentionally want to try native plugin management\,&#32;these commands change the user plugin installation state\.&#32;The scratch agent directory above is not a guarantee that plugin storage is project\-local\.

**Terminal shell—optional developer installation\:**

~~~sh
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\.&#32;`summary`&#32;defaults off\.

Launch normally\,&#32;without&#32;`--no-extensions`\,&#32;when checking ambient installed\-plugin discovery\.

`omp plugin install ./local-directory`&#32;routes to the same link operation\.&#32;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&#32;`node_modules`&#32;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&#32;`--scope project`&#32;does not make local\/npm installation project\-scoped\.&#32;The local\/npm install handler warns that scope is supported only for marketplace installs\.

Runtime discovery can read both user and project plugin roots\.&#32;Enabled project packages shadow user packages with the same package name\.&#32;That does not imply every installer writes to the project root\.

### Features and settings

Install\-spec feature syntax is\:

- `package`\:&#32;default features\;
- `package[summary]`\:&#32;selected features\;
- `package[*]`\:&#32;all optional features\;
- `package[]`\:&#32;no optional features\.

Quote bracketed shell arguments so your shell does not expand them\.&#32;Use a real package spec\;&#32;the workbook’s private teaching package is not claimed to exist in a registry\.

Plugin feature metadata supports&#32;`description`\,&#32;`default`\,&#32;and additional&#32;`extensions`\,&#32;`tools`\,&#32;`hooks`&#32;and&#32;`commands`\.

Plugin settings support\:

- string\,&#32;number\,&#32;boolean and enum types\;
- descriptions and defaults\;
- `secret`&#32;display masking\;
- an&#32;`env`&#32;fallback declaration\;
- numeric&#32;`min`\,&#32;`max`\,&#32;`step`\;
- enum&#32;`values`\.

A settings declaration is not executable behavior\.&#32;The supplied&#32;`getPluginSettings()`&#32;helpers merge saved global\/project values\;&#32;they do not inject a settings object into&#32;`ExtensionAPI`&#32;or magically apply all defaults\/environment fallbacks to extension code\.&#32;A consumer must resolve and validate its settings deliberately\.

The CLI supports&#32;`plugin config list|get|set|delete|validate`\.&#32;In this snapshot\,&#32;the parsed&#32;`--local`&#32;flag is not forwarded into the manager’s setter\:&#32;do not advertise it as a reliable project\-local write route\.

Project overrides have their own file\:

**Config\-file exercise—`.omp/plugin-overrides.json`\,&#32;selecting the real example feature\:**

~~~json
{
  "features": {
    "omp-workbook-field-notes": ["summary"]
  }
}
~~~

The same override structure supports&#32;`disabled`&#32;package names and per\-package&#32;`settings`\.

`secret: true`&#32;is not secure credential storage\.&#32;Human CLI display may mask a value while JSON output still exposes it\.&#32;Keep real credentials out of source\,&#32;screenshots\,&#32;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\;&#32;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\,&#32;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\;&#32;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\.&#32;Prefer namespaced flags so startup behavior is not surprising\.

The current CLI validates requested tool names against the fully discovered session registry\.&#32;Older custom\-tool documentation saying only built\-in names are validated is stale\.&#32;Still\,&#32;`--tools`\/`--no-tools`&#32;is not a universal isolation switch\:&#32;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()`&#32;reopens the current session file through&#32;`switchSession()`\.
- The supplied TUI command\-context&#32;`reload()`&#32;calls that session reload and rebuilds presentation\.
- RPC\/ACP plugin\-refresh code clears caches and refreshes skills\/file\-command metadata\,&#32;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\,&#32;restarting the explicit launch is the reliable workbook procedure\.&#32;Do not promise that&#32;`ctx.reload()`&#32;hot\-rebinds the factory\.

Treat&#32;`await ctx.reload(); return;`&#32;as the end of the current command handler\.&#32;Factory\-local notebook state need not reset on a session reload\.

The loader’s same\-process source cache\-busting also has platform\-specific behavior\.&#32;In particular\,&#32;the supplied compatibility loader notes a Windows&#32;`file://`&#32;query limitation\.&#32;A fresh process avoids treating an unverified hot\-reload path as proof\.

### Failure recovery and removal

An ordinary invalid export\,&#32;import failure or factory exception becomes a path\-specific loader error\;&#32;later modules still bind\.

Factory failure restores the queued provider\-registration list\,&#32;including earlier registrations the failed factory removed\.&#32;It is&#32;**not**&#32;a transaction over arbitrary side effects\.&#32;Recorded checks showed a failed factory’s flag default surviving that rollback\.

Inline&#32;`loadExtensionFromFactory()`&#32;rejects its promise instead of returning a per\-path loader error record\.

For recovery\:

1. Restart with only one explicit reviewed entry\.
2. Read the named load error\.
3. Check adjacent assets and dependencies\.
4. Restore a known\-good source version\.
5. Restart and inspect tool\/command metadata before invoking a mutation\.

For removal\:

- remove the explicit\/configured path\,&#32;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\:**

~~~sh
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\.&#32;A linked source directory remains your source\;&#32;uninstalling is not a request to delete that checkout\.

**Exercise\:**&#32;an extension remains active after disabling its plugin\.&#32;What is the first likely duplicate route to inspect\?

**Answer\:**&#32;an explicit&#32;`-e`&#32;or&#32;`extensions:`&#32;path\,&#32;or a loose copy in a native extension directory\.

*Source\,&#32;snapshot 2026\-08\-29\:&#32;`packages/coding-agent/src/extensibility/extensions/loader.ts`\;&#32;`packages/coding-agent/src/discovery/builtin.ts`\,&#32;`helpers.ts`\;&#32;`packages/coding-agent/src/extensibility/plugins/loader.ts`\,&#32;`manager.ts`\,&#32;`types.ts`\;&#32;`packages/coding-agent/src/cli/args.ts`\,&#32;`plugin-cli.ts`\;&#32;`packages/coding-agent/src/main.ts`\,&#32;`buildSessionOptions`\;&#32;`packages/coding-agent/src/session/agent-session.ts`\,&#32;`reload`\.*
