diff --git a/packages/coding-agent/src/extensibility/custom-commands/loader.ts b/packages/coding-agent/src/extensibility/custom-commands/loader.ts index dab47f8dc..28e314440 100644 --- a/packages/coding-agent/src/extensibility/custom-commands/loader.ts +++ b/packages/coding-agent/src/extensibility/custom-commands/loader.ts @@ -10,7 +10,6 @@ import { getAgentDir, getProjectDir, isEnoent, logger } from "@oh-my-pi/pi-utils import * as typebox from "@sinclair/typebox"; import { getConfigDirs } from "../../config"; import { execCommand } from "../../exec/exec"; -import { getPiRef, initPiRef } from "../pi-ref"; import { GreenCommand } from "./bundled/ci-green"; import { ReviewCommand } from "./bundled/review"; import type { @@ -169,7 +168,6 @@ function loadBundledCommands(sharedApi: CustomCommandAPI): LoadedCustomCommand[] * Discover and load custom commands from standard locations. */ export async function loadCustomCommands(options: LoadCustomCommandsOptions = {}): Promise { - await initPiRef(); const cwd = options.cwd ?? getProjectDir(); const agentDir = options.agentDir ?? getAgentDir(); @@ -185,7 +183,7 @@ export async function loadCustomCommands(options: LoadCustomCommandsOptions = {} exec: (command: string, args: string[], execOptions) => execCommand(command, args, execOptions?.cwd ?? cwd, execOptions), typebox, - pi: getPiRef(), + pi: await import("@oh-my-pi/pi-coding-agent"), }; // 1. Load bundled commands first (lowest priority - can be overridden) diff --git a/packages/coding-agent/src/extensibility/custom-tools/loader.ts b/packages/coding-agent/src/extensibility/custom-tools/loader.ts index 315f27359..651f53ca9 100644 --- a/packages/coding-agent/src/extensibility/custom-tools/loader.ts +++ b/packages/coding-agent/src/extensibility/custom-tools/loader.ts @@ -14,7 +14,6 @@ import { execCommand } from "../../exec/exec"; import type { HookUIContext } from "../../extensibility/hooks/types"; import { getAllPluginToolPaths } from "../../extensibility/plugins/loader"; import type { PendingActionStore } from "../../tools/pending-action"; -import { getPiRef, initPiRef } from "../pi-ref"; import { createNoOpUIContext, resolvePath } from "../utils"; import type { CustomToolAPI, CustomToolFactory, LoadedCustomTool, ToolLoadError } from "./types"; @@ -85,7 +84,12 @@ export class CustomToolLoader { #sharedApi: CustomToolAPI; #seenNames: Set; - constructor(cwd: string, builtInToolNames: string[], pendingActionStore?: PendingActionStore) { + constructor( + pi: typeof import("@oh-my-pi/pi-coding-agent"), + cwd: string, + builtInToolNames: string[], + pendingActionStore?: PendingActionStore, + ) { this.#sharedApi = { cwd, exec: (command: string, args: string[], options?: ExecOptions) => @@ -94,7 +98,7 @@ export class CustomToolLoader { hasUI: false, logger, typebox, - pi: getPiRef(), + pi, pushPendingAction: action => { if (!pendingActionStore) { throw new Error("Pending action store unavailable for custom tools in this runtime."); @@ -157,8 +161,12 @@ export async function loadCustomTools( builtInToolNames: string[], pendingActionStore?: PendingActionStore, ) { - await initPiRef(); - const loader = new CustomToolLoader(cwd, builtInToolNames, pendingActionStore); + const loader = new CustomToolLoader( + await import("@oh-my-pi/pi-coding-agent"), + cwd, + builtInToolNames, + pendingActionStore, + ); await loader.load(pathsWithSources); return { tools: loader.tools, @@ -185,7 +193,6 @@ export async function discoverAndLoadCustomTools( builtInToolNames: string[], pendingActionStore?: PendingActionStore, ) { - await initPiRef(); const allPathsWithSources: ToolPathWithSource[] = []; const seen = new Set(); diff --git a/packages/coding-agent/src/extensibility/extensions/loader.ts b/packages/coding-agent/src/extensibility/extensions/loader.ts index 852591582..79284f78a 100644 --- a/packages/coding-agent/src/extensibility/extensions/loader.ts +++ b/packages/coding-agent/src/extensibility/extensions/loader.ts @@ -17,7 +17,6 @@ import type { ExecOptions } from "../../exec/exec"; import { execCommand } from "../../exec/exec"; import type { CustomMessage } from "../../session/messages"; import { EventBus } from "../../utils/event-bus"; -import { getPiRef, initPiRef } from "../pi-ref"; import { getAllPluginExtensionPaths } from "../plugins/loader"; import { resolvePath } from "../utils"; import type { @@ -102,7 +101,6 @@ export class ExtensionRuntime implements IExtensionRuntime { class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime { readonly logger = logger; readonly typebox = TypeBox; - readonly pi = getPiRef(); readonly flagValues = new Map(); readonly pendingProviderRegistrations: Array<{ name: string; @@ -111,6 +109,7 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime { }> = []; constructor( + public readonly pi: typeof import("@oh-my-pi/pi-coding-agent"), private readonly extension: Extension, private readonly runtime: IExtensionRuntime, private readonly cwd: string, @@ -265,7 +264,13 @@ async function loadExtension( } const extension = createExtension(extensionPath, resolvedPath); - const api = new ConcreteExtensionAPI(extension, runtime, cwd, eventBus); + const api = new ConcreteExtensionAPI( + await import("@oh-my-pi/pi-coding-agent"), + extension, + runtime, + cwd, + eventBus, + ); await factory(api); return { extension, error: null }; @@ -285,9 +290,8 @@ export async function loadExtensionFromFactory( runtime: IExtensionRuntime, name = "", ): Promise { - await initPiRef(); const extension = createExtension(name, name); - const api = new ConcreteExtensionAPI(extension, runtime, cwd, eventBus); + const api = new ConcreteExtensionAPI(await import("@oh-my-pi/pi-coding-agent"), extension, runtime, cwd, eventBus); await factory(api); return extension; } @@ -296,7 +300,6 @@ export async function loadExtensionFromFactory( * Load extensions from paths. */ export async function loadExtensions(paths: string[], cwd: string, eventBus?: EventBus): Promise { - await initPiRef(); const extensions: Extension[] = []; const errors: Array<{ path: string; error: string }> = []; const resolvedEventBus = eventBus ?? new EventBus(); @@ -455,7 +458,6 @@ export async function discoverAndLoadExtensions( eventBus?: EventBus, disabledExtensionIds: string[] = [], ): Promise { - await initPiRef(); const allPaths: string[] = []; const seen = new Set(); const disabled = new Set(disabledExtensionIds); diff --git a/packages/coding-agent/src/extensibility/hooks/loader.ts b/packages/coding-agent/src/extensibility/hooks/loader.ts index ea7364328..a36bd6de9 100644 --- a/packages/coding-agent/src/extensibility/hooks/loader.ts +++ b/packages/coding-agent/src/extensibility/hooks/loader.ts @@ -9,7 +9,6 @@ import type { Hook } from "../../discovery"; import { loadCapability } from "../../discovery"; import type { HookMessage } from "../../session/messages"; import type { SessionManager } from "../../session/session-manager"; -import { getPiRef, initPiRef } from "../pi-ref"; import { resolvePath } from "../utils"; import { execCommand } from "./runner"; import type { ExecOptions, HookAPI, HookFactory, HookMessageRenderer, RegisteredCommand } from "./types"; @@ -87,16 +86,16 @@ export interface LoadHooksResult { * Create a HookAPI instance that collects handlers, renderers, and commands. * Returns the API, maps, and functions to set handlers later. */ -function createHookAPI( +async function createHookAPI( handlers: Map, cwd: string, -): { +): Promise<{ api: HookAPI; messageRenderers: Map; commands: Map; setSendMessageHandler: (handler: SendMessageHandler) => void; setAppendEntryHandler: (handler: AppendEntryHandler) => void; -} { +}> { let sendMessageHandler: SendMessageHandler | null = null; let appendEntryHandler: AppendEntryHandler | null = null; const messageRenderers = new Map(); @@ -137,7 +136,7 @@ function createHookAPI( }, logger, typebox, - pi: getPiRef(), + pi: await import("@oh-my-pi/pi-coding-agent"), } as HookAPI; return { @@ -170,7 +169,7 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo // Create handlers map and API const handlers = new Map(); - const { api, messageRenderers, commands, setSendMessageHandler, setAppendEntryHandler } = createHookAPI( + const { api, messageRenderers, commands, setSendMessageHandler, setAppendEntryHandler } = await createHookAPI( handlers, cwd, ); @@ -202,7 +201,6 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo * @param cwd - Current working directory for resolving relative paths */ export async function loadHooks(paths: string[], cwd: string): Promise { - await initPiRef(); const hooks: LoadedHook[] = []; const errors: Array<{ path: string; error: string }> = []; @@ -232,7 +230,6 @@ export async function loadHooks(paths: string[], cwd: string): Promise { - await initPiRef(); const allPaths: string[] = []; const seen = new Set(); diff --git a/packages/coding-agent/src/extensibility/pi-ref.ts b/packages/coding-agent/src/extensibility/pi-ref.ts deleted file mode 100644 index f13260bb2..000000000 --- a/packages/coding-agent/src/extensibility/pi-ref.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Lazy reference to the package barrel. - * - * Extension/hook/custom-tool/custom-command loaders expose the full - * `@oh-my-pi/pi-coding-agent` namespace to user code. A static - * `import * as piCodingAgent from "@oh-my-pi/pi-coding-agent"` from any of - * those loaders creates a self-referential cycle during module init: - * - * tools/index -> task -> sdk -> -> package barrel - * -> modes/components -> tool-execution -> renderers -> tools/read - * - * Combined with top-level await transitively pulled in by lru-cache, ESM - * interleaves the cyclic branches and leaves `readToolRenderer` in its - * temporal dead zone when `renderers.ts` reaches the `read` entry. - * - * This module breaks the cycle by deferring the barrel import. - * `initPiRef()` must be awaited once, after the package has finished its - * own initialization, before any loader reads the reference via - * `getPiRef()`. `createAgentSession` (and `discoverCustomTSCommands`) are - * responsible for this single call; both run entirely after module eval. - */ -import type * as PiCodingAgentNs from "@oh-my-pi/pi-coding-agent"; - -type PiCodingAgent = typeof PiCodingAgentNs; - -let cached: PiCodingAgent | undefined; - -/** - * Resolve the package barrel once. Safe to call from any async entry point - * that runs after all modules have finished evaluating. - */ -export async function initPiRef(): Promise { - if (cached) return; - cached = (await import("@oh-my-pi/pi-coding-agent")) as PiCodingAgent; -} - -/** - * Return the cached package barrel. `initPiRef()` must have resolved - * before this is called; loaders that run inside `createAgentSession` - * or `discoverCustomTSCommands` satisfy that requirement automatically. - */ -export function getPiRef(): PiCodingAgent { - if (!cached) { - throw new Error("pi-ref not initialized; await initPiRef() before instantiating loaders that expose `pi`."); - } - return cached; -} diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index bd0379b17..21a6e5cad 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -58,7 +58,6 @@ import { type ToolDefinition, wrapRegisteredTools, } from "./extensibility/extensions"; -import { initPiRef } from "./extensibility/pi-ref"; import { loadSkills as loadSkillsInternal, type Skill, type SkillWarning } from "./extensibility/skills"; import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal } from "./extensibility/slash-commands"; import { @@ -350,7 +349,6 @@ export async function discoverSlashCommands(cwd?: string): Promise { - await initPiRef(); const resolvedCwd = cwd ?? getProjectDir(); const resolvedAgentDir = agentDir ?? getDefaultAgentDir(); @@ -642,7 +640,6 @@ function buildMCPPromptCommands(manager: MCPManager): LoadedCustomCommand[] { * ``` */ export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise { - await initPiRef(); const cwd = options.cwd ?? getProjectDir(); const agentDir = options.agentDir ?? getDefaultAgentDir(); const eventBus = options.eventBus ?? new EventBus(); diff --git a/packages/coding-agent/test/python-tool-settings.test.ts b/packages/coding-agent/test/python-tool-settings.test.ts index f92737b2d..9ddb31dde 100644 --- a/packages/coding-agent/test/python-tool-settings.test.ts +++ b/packages/coding-agent/test/python-tool-settings.test.ts @@ -56,7 +56,7 @@ describe("python tool settings", () => { }); it("passes kernel mode from settings to executor", async () => { - vi.spyOn(pythonExecutor, "warmPythonEnvironment").mockResolvedValue({ ok: true }); + vi.spyOn(pythonExecutor, "warmPythonEnvironment").mockResolvedValue({ ok: true, docs: [] }); const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockResolvedValue({ output: "ok", exitCode: 0, diff --git a/packages/coding-agent/test/tools/python-execution.test.ts b/packages/coding-agent/test/tools/python-execution.test.ts index 3a7c1083c..463cbc6be 100644 --- a/packages/coding-agent/test/tools/python-execution.test.ts +++ b/packages/coding-agent/test/tools/python-execution.test.ts @@ -23,7 +23,7 @@ function createSession(cwd: string): ToolSession { describe("python tool execution", () => { it("passes kernel options from settings and args", async () => { const tempDir = TempDir.createSync("@python-tool-"); - vi.spyOn(pythonExecutor, "warmPythonEnvironment").mockResolvedValue({ ok: true }); + vi.spyOn(pythonExecutor, "warmPythonEnvironment").mockResolvedValue({ ok: true, docs: [] }); const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockResolvedValue({ output: "ok", exitCode: 0,