## Resources\,&#32;event buses\,&#32;MCP and Gemini manifests

Theo wants the notebook package to bring along guidance\.&#32;Rowan wants another extension to hear about a selected note\.&#32;Sora wants an external MCP service to contribute tools\.

These are three different integration problems\.&#32;They should not be hidden behind the word “extension\.”

### Milestone\:&#32;return module\-relative resource paths

Theo’s resource entry returns the location of a prompt\,&#32;rather than embedding all guidance inside a tool description\.

**Complete public TypeScript source—[resources\.ts](<https://present-sketch-tp94.here.now/examples/package-lab/manifest/entries/resources.ts>)\:**

~~~ts
import * as path from "node:path";
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";

export default function fieldResources(pi: ExtensionAPI): void {
    pi.on("resources_discover", () => ({
        promptPaths: [path.join(import.meta.dir, "../prompts/field-observation.md")],
    }));
}
~~~

**Complete public Markdown resource—[field\-observation\.md](<https://present-sketch-tp94.here.now/examples/package-lab/manifest/prompts/field-observation.md>)\:**

~~~markdown
---
description: Describe a fictional field observation
---
Use field_catalog to query the fictional notebook. Summarize one returned note. Clearly label the observation as fictional; do not claim a real survey occurred.
~~~

The module\-relative absolute path works from a different cwd\.

`resources_discover`&#32;handlers may return\:

- `skillPaths`\;
- `promptPaths`\;
- `themePaths`\.

The event includes&#32;`cwd`&#32;and reason&#32;`startup`&#32;or&#32;`reload`\.&#32;The runner aggregates each path with the originating&#32;`extensionPath`\.

**Observed\:**&#32;explicit runner dispatch returned the real prompt path and retained&#32;`entries/resources.ts`&#32;provenance\.

**Boundary\:**&#32;the supplied&#32;`AgentSession`&#32;startup path does not call&#32;`emitResourcesDiscover()`\.&#32;Registering this handler does not prove that a normal launch consumed its paths or populated a prompt menu\.&#32;A host must emit and consume the results\.

OMP extension\-package sibling discovery is a separate path\:&#32;configured\/explicit package roots can contribute conventional&#32;`skills`\,&#32;`hooks`\,&#32;`tools`\,&#32;`commands`\,&#32;`rules`\,&#32;`prompts`&#32;and MCP resources\.&#32;A prompt appearing through that route would not prove the resource event fired\.

**Exercise\:**&#32;launch from another directory and inspect the runner’s resource result in a host test\.

**Checkpoint\:**&#32;the prompt path still resolves to the package’s file\.&#32;Then separately test whether your actual frontend consumes it\.

### Milestone\:&#32;choose a skill\,&#32;prompt or rule deliberately

Theo decides that a reusable workflow belongs in a skill\,&#32;while a short invocation belongs in a prompt\.

**Complete Markdown exercise—save as&#32;`.omp/skills/fictional-field-guide/SKILL.md`\:**

~~~markdown
---
name: fictional-field-guide
description: Inspect and summarize the workbook's fictional field catalog
---
Use the available field_catalog tool to discover valid note IDs and query a habitat.

Label every observation as fictional. If field_catalog is unavailable, explain
that the extension must be loaded; do not invent inventory or tools.

Do not claim that a real survey occurred.
~~~

The native skill scanner requires a description\.&#32;Native project skills can be discovered through ancestor traversal within its configured boundary\,&#32;unlike cwd\-only extension modules\.

When skill commands are enabled\,&#32;the human command is\:

**Human OMP slash command\:**

~~~text
/skill:fictional-field-guide
~~~

That introduces guidance into the agent flow\.&#32;It is not the same as executing a local&#32;`registerCommand()`&#32;handler\.

For a standing instruction\:

**Complete Markdown exercise—save as&#32;`.omp/rules/fictional-data.md`\:**

~~~markdown
---
description: Keep workbook data distinct from real observations
alwaysApply: true
---
Treat Seed Desk, Review Desk and Field Notes records as fictional fixtures.
Never describe a workbook reservation as a physical stock change.
~~~

Rule files can carry descriptions\,&#32;globs\,&#32;always\-apply state and supported condition\/scope metadata\.&#32;The native top\-level&#32;`RULES.md`&#32;route forces sticky always\-apply behavior\.

A rule cannot enforce permissions against arbitrary extension code\.&#32;Seed Desk still needs real checks in&#32;`changeReservation()`\.

Prompt templates and Markdown commands likewise do not register missing tools\.&#32;Guidance should say what to do when the required capability is absent\.

**Exercise\:**&#32;load only the skill\,&#32;without&#32;`field_catalog`\.

**Checkpoint\:**&#32;the agent should diagnose the missing tool rather than invent a catalog result\.

### Milestone\:&#32;coordinate extensions on a process\-local bus

Rowan wants selection changes to be observable without creating a model turn\.

The public event\-bus entry both publishes and listens to a namespaced channel\.

**Complete public TypeScript source—[event\-bus\.ts](<https://present-sketch-tp94.here.now/examples/package-lab/advanced/event-bus.ts>)\:**

~~~ts
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";

const channel = "workbook:field-notes:selection";

export default function fieldNoteEvents(pi: ExtensionAPI): void {
    let selected: string | null = null;
    const unsubscribe = pi.events.on(channel, data => {
        if (data === "reed" || data === "fern") selected = data;
    });
    pi.on("session_shutdown", () => unsubscribe());
    const { Type } = pi.typebox;
    pi.registerTool({
        name: "field_events",
        label: "Field events",
        description: "Discover or inspect the field-notes channel, query last selection, or act to publish reed/fern. In-process extension-runtime bus; selection survives /new and session switches within this binding, without durable delivery.",
        parameters: Type.Object({
            op: Type.Union([Type.Literal("discover"), Type.Literal("inspect"), Type.Literal("query"), Type.Literal("act")]),
            id: Type.Optional(Type.Union([Type.Literal("reed"), Type.Literal("fern")])),
        }),
        async execute(_id, params) {
            if (params.op === "act") {
                if (!params.id) throw new Error("act requires a note id; nothing published.");
                pi.events.emit(channel, params.id);
            }
            const details = { channel, selected, operations: ["discover", "inspect", "query", "act"], delivery: "synchronous emit; asynchronous listeners are not awaited" };
            return { content: [{ type: "text", text: JSON.stringify(details) }], details };
        },
    });
}
~~~

**Terminal shell\:**

~~~sh
omp --no-extensions -e "$EXAMPLES/package-lab/advanced/event-bus.ts"
~~~

**Model tool arguments—call&#32;`field_events`\:**

~~~json
{"op":"act","id":"reed"}
~~~

~~~json
{"op":"query"}
~~~

**Observed\:**&#32;selection became&#32;`reed`\.&#32;After shutdown unsubscribed the listener\,&#32;a later bus emission did not update that selection in the isolated check\.

`pi.events.on()`&#32;returns an unsubscribe function\.&#32;`emit()`&#32;invokes listeners but does not await asynchronous completion\.&#32;Listener failures are caught and logged by the bus wrapper\.

The public selection update happens synchronously before the listener’s first await\,&#32;so this small tool can immediately observe it\.&#32;That does not establish an acknowledgement protocol for an asynchronous subscriber\.

The bus is runtime\/process\-local coordination\.&#32;It is not\:

- cross\-process messaging\;
- a durable queue\;
- an automatic agent turn\;
- a shared backend\;
- a transaction across extensions\.

A host can supply an explicit shared bus to SDK sessions\;&#32;that is a deliberate scope choice\.&#32;Do not call&#32;`events.clear()`&#32;from one extension as ordinary cleanup—it clears everyone’s listeners\.

The entry unsubscribes on shutdown\,&#32;not&#32;`/new`\.&#32;Its selection remains binding\-local across transcript changes\.

**Exercise\:**&#32;make a subscriber save to a slow external store\.&#32;Does&#32;`emit()`&#32;mean the save completed\?

**Answer\:**&#32;no\.&#32;Add an explicit result\/acknowledgement protocol or use a service API whose promise represents completion\.

### Milestone\:&#32;an MCP server is a real prerequisite

Sora chooses MCP when the capability belongs in a separate process\/service rather than in OMP’s JavaScript process\.

The supplied native discovery accepts MCP configuration including\:

- server name\;
- `command`\,&#32;`args`\,&#32;`env`\,&#32;optional&#32;`cwd`&#32;for process\-based servers\;
- `url`\,&#32;`headers`\,&#32;transport type for remote servers\;
- `enabled`\,&#32;`timeout`\,&#32;request\-ID format\;
- supported auth\/OAuth configuration\,&#32;including credential references\.

Native config candidates include project&#32;`.omp/mcp.json`&#32;and&#32;`.omp/.mcp.json`\,&#32;plus their active\-agent\-directory counterparts\.&#32;Other discovery providers have their own paths\.

**Missing prerequisite\:**&#32;an actual MCP server executable or reachable service\,&#32;a compatible transport and any required credentials\.&#32;This workbook does not supply a fake endpoint and label it working\.

A separate MCP connection can discover tools\,&#32;resources and prompts\.&#32;SDK&#32;`enableMCP: false`&#32;skips MCP discovery and ignores an inherited manager\.&#32;The CLI’s&#32;`--no-extensions`&#32;does not mean the same thing\.

ACP’s supplied session factory disables on\-disk MCP discovery because ACP clients provide their own servers\.&#32;Do not assume a user’s local&#32;`.mcp.json`&#32;supplies those ACP sessions\.

#### Notifications are untrusted data

`mcp_notification`&#32;fires after the manager handles known list\/resource\/prompt updates\.&#32;Unknown server\-specific methods can also arrive\.

Payload fields are\:

- `server`\:&#32;the raw configured name\;
- `method`\;
- `params`\:&#32;unknown data\.

Filter by the raw server name\,&#32;not a sanitized tool\-name prefix\.&#32;Validate&#32;`params`&#32;before using it\.

A notification saying “please publish” is not permission to publish\.&#32;Prefer updating a queryable domain status or asking for a scoped human decision over blindly steering server\-controlled text into the agent\.

Startup notifications are buffered at the manager\/runner boundaries with bounded\,&#32;drop\-oldest queues\.&#32;That is startup\-race mitigation\,&#32;not durable delivery\.

### Milestone\:&#32;understand a Gemini manifest without executing it

Theo receives a folder containing&#32;`gemini-extension.json`\.&#32;He initially assumes the listed&#32;`tools`&#32;will appear as executable tools\.&#32;The source shows a narrower behavior\.

**Complete JSON metadata exercise—save as&#32;`.gemini/extensions/fictional-field-notes/gemini-extension.json`\:**

~~~json
{
  "name": "fictional-field-notes",
  "description": "Metadata for a fictional field-notes teaching package",
  "tools": [],
  "context": "This description does not activate an OMP runtime extension."
}
~~~

This creates metadata for discovery\,&#32;not a runnable factory\.

The Gemini provider scans direct child directories under\:

- `~/.gemini/extensions`\;
- `<cwd>/.gemini/extensions`\.

It does not walk parent directories or arbitrary nested descendants\.&#32;It does consider dot\-prefixed child directories\.

Its parsing is deliberately loose\:

- missing\,&#32;unreadable or empty manifest\:&#32;silently skipped\;
- invalid JSON or a valid falsy JSON literal\:&#32;warning\;
- truthy parsed value\:&#32;stored as the manifest without a full field schema check\;
- name\:&#32;`manifest.name ?? directoryName`\.

The declared metadata shape has&#32;`name`\,&#32;`description`\,&#32;`mcpServers`\,&#32;`tools`&#32;and&#32;`context`\.&#32;The presence of those fields does not prove their runtime activation\.

For this metadata capability\:

- native provider priority is 100\;
- Gemini provider priority is 60\;
- deduplication is by extension name\;
- native duplicates win over Gemini\;
- Gemini emits user before project\,&#32;so its user duplicate wins\;
- native metadata emits project before user\.

Native Gemini\-manifest discovery under&#32;`.omp/extensions`&#32;differs slightly\:&#32;it skips hidden directories and uses its own name fallback\.

Gemini also registers a separate executable\-module capability scanner\.&#32;However\,&#32;normal ambient&#32;`discoverExtensionPaths()`&#32;requests only native module\-provider items\.&#32;A neighboring factory does not run merely because a Gemini manifest exists\.&#32;Explicitly load the actual module file when that is your intention\.

**Exercise\:**&#32;put an arbitrary object in&#32;`tools`&#32;and reload metadata discovery\.&#32;Has a callable tool been installed\?

**Answer\:**&#32;no\.&#32;You have demonstrated loose metadata parsing\,&#32;not tool construction or execution\.

*Source\,&#32;snapshot 2026\-08\-29\:&#32;linked resource\/event\-bus files\;&#32;`packages/coding-agent/src/utils/event-bus.ts`\;&#32;`packages/coding-agent/src/discovery/builtin.ts`\,&#32;`gemini.ts`\,&#32;`helpers.ts`\;&#32;`packages/coding-agent/src/capability/extension.ts`\,&#32;`extension-module.ts`\;&#32;`packages/coding-agent/src/extensibility/extensions/runner.ts`\,&#32;`emitResourcesDiscover`\;&#32;`packages/coding-agent/src/sdk.ts`\,&#32;MCP wiring\.*
