Prepare a reversible lab
Before adding convenience, make failure inexpensive.
Mara, a seed-library volunteer, wants to experiment without filling her normal project with extension state. She chooses an explicit entry path and a scratch working directory. That gives her a reproducible launch command and a clear place to inspect the session file.
Prerequisites
You need:
- The workbook-compatible custom
omponPATH. - Bun; the recorded checks used Bun 1.3.14.
- The complete example directory structure, preferably from the ZIP.
- Matching runtime dependencies where an example imports them.
Check the installed versions, but do not treat a matching version string as proof that an unrelated distribution contains the same custom APIs.
Terminal shell—inspect your installed programs:
omp --version
bun --version
The Seed Desk stages are complete, independent entries. Load one at a time: all three intentionally own /seeds.
Package Lab’s command-line entries use type-only SDK imports and injected schema builders. Seed Desk stage 3 additionally imports matching @oh-my-pi/omptype and @oh-my-pi/pi-tui runtime packages. Review Desk imports the TUI package through the compatible host’s extension loader.
Do not fix a missing custom package by silently substituting an incompatible upstream package.
Establish the workspace once
Start in the extracted directory that contains seed-desk, review-desk and package-lab.
Terminal shell—create the workbook workspace:
EXAMPLES="$PWD"
LAB="$(mktemp -d)"
mkdir -p "$LAB/work" "$LAB/agent"
export PI_CODING_AGENT_DIR="$LAB/agent"
export PI_PROFILE=
cd "$LAB/work"
Later commands assume these shell variables remain available.
This is a reversible workspace, not complete isolation:
PI_CODING_AGENT_DIRselects the lab’s agent directory.--no-extensionssuppresses ambient extension-factory discovery, while explicit entries still load.- Other discovery families, environment credentials, package installation and host startup activity have their own behavior.
- Neither an empty working directory nor
--no-sessionis an OS security boundary.
The recorded isolated scenarios used an additional OS policy denying network access and restricting writes. Ordinary reader launch commands below do not install that policy.
An extension is a factory, not a command-line program
A module loaded with -e exports a default function. OMP calls that function with ExtensionAPI.
Complete TypeScript exercise—save as lab-status.ts in the lab working directory:
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
export default function labStatus(pi: ExtensionAPI): void {
pi.setLabel("Workbook lab");
pi.on("session_start", (_event, ctx) => {
pi.logger.debug("Workbook lab session started");
if (ctx.hasUI) {
ctx.ui.notify("Workbook lab loaded.", "info");
}
});
}
Terminal shell—load that exercise:
omp --no-extensions -e ./lab-status.ts
Expected checkpoint: in a TUI, the session-start handler produces Workbook lab loaded. In a default headless context, the notification is absent; that absence is not a load failure.
Running bun lab-status.ts merely evaluates a module that exports a function. It does not supply OMP’s extension runtime.
Registration first, initialized actions later
The lifecycle has two important phases:
- Factory binding: register commands, tools, flags, handlers, renderers and fallback handlers.
- Runtime initialization: the host wires session actions and UI, then dispatches events and invocations.
Methods such as sendMessage(), appendEntry(), getAllTools() and setModel() depend on the initialized runtime. Calling them during factory loading raises ExtensionRuntimeNotInitializedError.
An async factory is valid. Seed Desk uses one to read tool.txt before registering a tool. Async initialization does not make runtime actions available early.
Register providers at factory time if needed: their registrations are queued for the model registry. That is a special registration path, not an exception allowing arbitrary session actions during loading.
Keep the lifetimes separate
| Lifetime | What belongs here | What does not follow automatically |
|---|---|---|
| Process | Loaded libraries, process-wide registries, explicitly shared buses | One user, one session or one authorization scope |
| Module evaluation | Static fixtures and helper definitions | Fresh mutable state for every SDK session |
| Factory binding | Closures created when the factory is called | Automatic reset on /new or session switching |
| Session/transcript | A session ID, header and journal | A global shared database |
| Current branch | The path from root to the active leaf | Every entry in the session file |
| Invocation | The current handler/tool context and abort signal | A context object safe to cache indefinitely |
The Field Notes selection later demonstrates the distinction: it survives a transcript change because the factory binding survives.
Trust is already being granted
Source-backed: ExtensionContext.isProjectTrusted() always returns true in this build. It is a compatibility method reflecting that project-local inputs are already trusted by default. It is not a prompt, sandbox or per-directory permission store.
Review the selected module’s entire import graph. Package dependencies and helper modules have the same in-process JavaScript authority as the entry.
Exercise: move pi.sendMessage() into the factory body of a copy of lab-status.ts. What should happen?
Answer: loading should report an uninitialized-runtime error. Move the action into a handler; do not add a delay and hope startup finishes first.
Source, snapshot 2026-08-29: packages/coding-agent/src/extensibility/extensions/loader.ts, getExtensionFactory, ExtensionRuntime, ConcreteExtensionAPI; packages/coding-agent/src/extensibility/extensions/runner.ts, initialize, createContext.
Extensions inside those boundaries · Source chapter: extensions/prepare-a-reversible-lab. Original evidence remains scoped to its recorded snapshot.