diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index f146ac35d..60333a329 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Prevented managed git packages from automatically installing Pi peer dependencies and added warnings for extension packages that list host-provided modules in `dependencies` ([#9863](https://github.com/earendil-works/pi/issues/9863)). + ## [0.87.1] - 2026-09-22 ### New Features diff --git a/packages/coding-agent/docs/packages.md b/packages/coding-agent/docs/packages.md index 018eb219e..0d5de1cb6 100644 --- a/packages/coding-agent/docs/packages.md +++ b/packages/coding-agent/docs/packages.md @@ -85,7 +85,9 @@ Pi supplies these packages to extensions and skills: - `@earendil-works/pi-tui` - `typebox` -Declare imported Pi packages in `peerDependencies` with a `"*"` range and do not bundle them. Other Pi packages used as dependencies must be included in the published tarball and referenced through their `node_modules` resource paths. +Declare the host-provided packages listed above in `peerDependencies` with a `"*"` range and do not bundle them. Pi suppresses automatic peer installation for managed npm packages and git packages installed with npm, pnpm, or Bun. Local packages are not installed or modified, so their dependency tree remains the package author's responsibility. + +Do not list host-provided packages in `dependencies`. A physical copy can bypass Pi's extension module mapping in compiled ESM and create duplicate classes, registries, and initialization work. Pi reports an extension warning when it detects this manifest configuration. Other Pi packages used as dependencies must be included in the published tarball and referenced through their `node_modules` resource paths. Installed packages load with separate module roots. Do not rely on two packages sharing one dependency instance or one package resolving another package’s undeclared dependency. diff --git a/packages/coding-agent/src/core/extensions/loader.ts b/packages/coding-agent/src/core/extensions/loader.ts index ab7ec239a..a957a3488 100644 --- a/packages/coding-agent/src/core/extensions/loader.ts +++ b/packages/coding-agent/src/core/extensions/loader.ts @@ -605,6 +605,7 @@ async function loadExtensionsInternal( ): Promise { const extensions: Extension[] = []; const errors: Array<{ path: string; error: string }> = []; + const warnings: Array<{ path: string; warning: string }> = []; const cacheToken = useCache ? useExtensionCacheCwd(cwd) : undefined; const resolvedCwd = cacheToken?.cwd ?? resolvePath(cwd); const resolvedEventBus = eventBus ?? createEventBus(); @@ -632,6 +633,7 @@ async function loadExtensionsInternal( return { extensions, errors, + warnings, runtime: resolvedRuntime, }; } diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index 41bc4e672..48b12ccc9 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -1909,6 +1909,7 @@ export interface Extension { export interface LoadExtensionsResult { extensions: Extension[]; errors: Array<{ path: string; error: string }>; + warnings?: Array<{ path: string; warning: string }>; /** Shared runtime - actions are throwing stubs until runner.initialize() */ runtime: ExtensionRuntime; } diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index d65386c62..eb6f47825 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -69,6 +69,7 @@ export interface PathMetadata { scope: SourceScope; origin: "package" | "top-level"; baseDir?: string; + packageRoot?: string; } export interface ResolvedResource { @@ -1291,6 +1292,7 @@ export class DefaultPackageManager implements PackageManager { installedPath = this.getNpmInstallPath(parsed, resolvedScope); } metadata.baseDir = installedPath; + metadata.packageRoot = installedPath; this.collectPackageResources(installedPath, accumulator, filter, metadata); continue; } @@ -1304,6 +1306,7 @@ export class DefaultPackageManager implements PackageManager { await this.refreshTemporaryGitSource(parsed, resolvedSource); } metadata.baseDir = installedPath; + metadata.packageRoot = installedPath; this.collectPackageResources(installedPath, accumulator, filter, metadata); } } @@ -1345,6 +1348,7 @@ export class DefaultPackageManager implements PackageManager { } if (stats.isDirectory()) { metadata.baseDir = resolved; + metadata.packageRoot = resolved; const resources = this.collectPackageResources(resolved, accumulator, filter, metadata); if (!resources) { this.addResource(accumulator.extensions, resolved, metadata, true); @@ -1758,10 +1762,25 @@ export class DefaultPackageManager implements PackageManager { private getPackageManagerName(): string { const npmCommand = this.getNpmCommand(); - const commandParts = [npmCommand.command, ...npmCommand.args]; - const separatorIndex = commandParts.lastIndexOf("--"); - const packageManagerCommand = separatorIndex >= 0 ? commandParts[separatorIndex + 1] : npmCommand.command; - return packageManagerCommand ? basename(packageManagerCommand).replace(/\.(cmd|exe)$/i, "") : ""; + const normalizeCommandName = (command: string): string => basename(command).replace(/\.(cmd|exe)$/i, ""); + const supportedPackageManagers = new Set(["npm", "pnpm", "bun"]); + const directCommand = normalizeCommandName(npmCommand.command); + const separatorIndex = npmCommand.args.lastIndexOf("--"); + if (separatorIndex >= 0) { + const wrappedCommand = npmCommand.args[separatorIndex + 1]; + return wrappedCommand ? normalizeCommandName(wrappedCommand) : directCommand; + } + if (supportedPackageManagers.has(directCommand)) return directCommand; + + const wrappedPackageManagers = [ + ...new Set( + npmCommand.args.map(normalizeCommandName).filter((command) => supportedPackageManagers.has(command)), + ), + ]; + if (wrappedPackageManagers.length > 1) { + throw new Error(`Ambiguous npmCommand package managers: ${wrappedPackageManagers.join(", ")}`); + } + return wrappedPackageManagers[0] ?? directCommand; } private async runNpmCommand(args: string[], options?: { cwd?: string }): Promise { @@ -1770,11 +1789,22 @@ export class DefaultPackageManager implements PackageManager { } private getGitDependencyInstallArgs(): string[] { - const configuredCommand = this.settingsManager.getNpmCommand(); - if (configuredCommand && configuredCommand.length > 0) { - return ["install"]; + switch (this.getPackageManagerName()) { + case "bun": + return ["install", "--omit=dev", "--omit=peer"]; + case "pnpm": + return [ + "install", + "--prod", + "--config.auto-install-peers=false", + "--config.strict-peer-dependencies=false", + "--config.strict-dep-builds=false", + ]; + case "npm": + return ["install", "--omit=dev", "--legacy-peer-deps"]; + default: + return ["install"]; } - return ["install", "--omit=dev"]; } private runNpmCommandSync(args: string[]): string { diff --git a/packages/coding-agent/src/core/resource-loader.ts b/packages/coding-agent/src/core/resource-loader.ts index 6babd0178..204c9c3d2 100644 --- a/packages/coding-agent/src/core/resource-loader.ts +++ b/packages/coding-agent/src/core/resource-loader.ts @@ -37,6 +37,61 @@ export interface ResourceLoaderReloadOptions { resolveProjectTrust?: (input: { extensionsResult: LoadExtensionsResult }) => Promise; } +const HOST_PROVIDED_EXTENSION_PACKAGES = new Set([ + "@earendil-works/pi-agent-core", + "@earendil-works/pi-ai", + "@earendil-works/pi-coding-agent", + "@earendil-works/pi-tui", + "@mariozechner/pi-agent-core", + "@mariozechner/pi-ai", + "@mariozechner/pi-coding-agent", + "@mariozechner/pi-tui", + "@sinclair/typebox", + "typebox", +]); + +function collectExtensionPackageWarnings( + extensionPaths: string[], + metadataByPath: Map, +): Array<{ path: string; warning: string }> { + const warnings: Array<{ path: string; warning: string }> = []; + const packageRoots = new Set( + extensionPaths + .map((extensionPath) => metadataByPath.get(extensionPath)?.packageRoot) + .filter((packageRoot): packageRoot is string => packageRoot !== undefined), + ); + for (const packageRoot of packageRoots) { + const packageJsonPath = join(packageRoot, "package.json"); + if (!existsSync(packageJsonPath)) continue; + const manifest = JSON.parse(stripBom(readFileSync(packageJsonPath, "utf-8"))) as { dependencies?: unknown }; + if ( + typeof manifest.dependencies !== "object" || + manifest.dependencies === null || + Array.isArray(manifest.dependencies) + ) { + continue; + } + const hostDependencies = Object.keys(manifest.dependencies) + .filter((name) => HOST_PROVIDED_EXTENSION_PACKAGES.has(name)) + .sort(); + if (hostDependencies.length === 0) continue; + warnings.push({ + path: packageJsonPath, + warning: `Host-provided extension packages must be declared in peerDependencies with a "*" range, not dependencies: ${hostDependencies.join(", ")}. Installed copies can bypass the extension loader and create duplicate runtime modules.`, + }); + } + return warnings; +} + +function mergeExtensionWarnings( + result: LoadExtensionsResult, + warnings: Array<{ path: string; warning: string }>, +): void { + result.warnings = [ + ...new Map([...(result.warnings ?? []), ...warnings].map((warning) => [warning.path, warning])).values(), + ]; +} + export interface ResourceLoader { getExtensions(): LoadExtensionsResult; getSkills(): { skills: Skill[]; diagnostics: ResourceDiagnostic[] }; @@ -453,7 +508,9 @@ export class DefaultResourceLoader implements ResourceLoader { ? cliEnabledExtensions : this.mergePaths(cliEnabledExtensions, enabledExtensions); + const packageWarnings = collectExtensionPackageWarnings(extensionPaths, metadataByPath); const extensionsResult = await this.loadFinalExtensionSet(extensionPaths, preTrustExtensions); + mergeExtensionWarnings(extensionsResult, packageWarnings); for (const p of this.additionalExtensionPaths) { if (isLocalPath(p)) { const resolved = this.resolveResourcePath(p); @@ -556,7 +613,15 @@ export class DefaultResourceLoader implements ResourceLoader { const extensionPaths = this.noExtensions ? cliEnabledExtensions : this.mergePaths(cliEnabledExtensions, enabledExtensions); + const metadataByPath = new Map( + [...resolvedPaths.extensions, ...cliExtensionPaths.extensions].map((resource) => [ + resource.path, + resource.metadata, + ]), + ); + const packageWarnings = collectExtensionPackageWarnings(extensionPaths, metadataByPath); const extensionsResult = await loadExtensionsCached(extensionPaths, this.cwd, this.eventBus); + mergeExtensionWarnings(extensionsResult, packageWarnings); if (!options.includeInlineFactories) { return extensionsResult; } @@ -618,6 +683,7 @@ export class DefaultResourceLoader implements ResourceLoader { const extensionsResult: LoadExtensionsResult = { extensions: orderedExtensions, errors: [...preTrustExtensions.errors, ...remainingExtensions.errors], + warnings: [...(preTrustExtensions.warnings ?? []), ...(remainingExtensions.warnings ?? [])], runtime: preTrustExtensions.runtime, }; this.addExtensionConflictDiagnostics(extensionsResult); diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 195a1127e..12fe094b3 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -787,6 +787,10 @@ export async function main(args: string[], options?: MainOptions) { type: "error" as const, message: `Failed to load extension "${path}": ${error}`, })), + ...(resourceLoader.getExtensions().warnings ?? []).map(({ path, warning }) => ({ + type: "warning" as const, + message: `Extension package "${path}": ${warning}`, + })), ]; const modelPatterns = parsed.models ?? settingsManager.getEnabledModels(); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 968f74140..a502e43d2 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -1872,11 +1872,12 @@ export class InteractiveMode { } const extensionDiagnostics: ResourceDiagnostic[] = []; - const extensionErrors = this.session.resourceLoader.getExtensions().errors; - if (extensionErrors.length > 0) { - for (const error of extensionErrors) { - extensionDiagnostics.push({ type: "error", message: error.error, path: error.path }); - } + const extensionsResult = this.session.resourceLoader.getExtensions(); + for (const error of extensionsResult.errors) { + extensionDiagnostics.push({ type: "error", message: error.error, path: error.path }); + } + for (const warning of extensionsResult.warnings ?? []) { + extensionDiagnostics.push({ type: "warning", message: warning.warning, path: warning.path }); } const commandDiagnostics = this.session.extensionRunner.getCommandDiagnostics(); diff --git a/packages/coding-agent/test/extensions-discovery.test.ts b/packages/coding-agent/test/extensions-discovery.test.ts index ebb9b464c..0448984f7 100644 --- a/packages/coding-agent/test/extensions-discovery.test.ts +++ b/packages/coding-agent/test/extensions-discovery.test.ts @@ -69,6 +69,41 @@ describe("extensions discovery", () => { expect(result.extensions).toHaveLength(1); }); + it("does not infer package ownership from ancestor manifests", async () => { + // Regression for #9863. + const dependencyDir = path.join(tempDir, "node_modules", "@earendil-works", "pi-coding-agent"); + fs.mkdirSync(dependencyDir, { recursive: true }); + fs.writeFileSync( + path.join(tempDir, "package.json"), + JSON.stringify({ + name: "application", + type: "module", + dependencies: { "@earendil-works/pi-coding-agent": "1.0.0" }, + }), + ); + fs.writeFileSync( + path.join(dependencyDir, "package.json"), + JSON.stringify({ name: "@earendil-works/pi-coding-agent", type: "module", exports: "./index.js" }), + ); + fs.writeFileSync(path.join(dependencyDir, "index.js"), "export const physicalDependency = true;"); + fs.writeFileSync( + path.join(extensionsDir, "compiled-esm-extension.js"), + ` + import { physicalDependency } from "@earendil-works/pi-coding-agent"; + export default function(pi) { + if (physicalDependency) pi.registerCommand("physical-dependency", { handler: async () => {} }); + } + `, + ); + + const result = await discoverAndLoadExtensions([], tempDir, tempDir); + + expect(result.errors).toEqual([]); + expect(result.extensions).toHaveLength(1); + expect(result.extensions[0].commands.has("physical-dependency")).toBe(true); + expect(result.warnings).toEqual([]); + }); + it("keeps the type-only pi-ai OAuth compatibility barrel resolvable", async () => { fs.writeFileSync( path.join(extensionsDir, "oauth-import.ts"), diff --git a/packages/coding-agent/test/package-manager.test.ts b/packages/coding-agent/test/package-manager.test.ts index 6f4f6b4fa..c93099b81 100644 --- a/packages/coding-agent/test/package-manager.test.ts +++ b/packages/coding-agent/test/package-manager.test.ts @@ -27,6 +27,8 @@ class MockSpawnedProcess extends EventEmitter { interface PackageManagerInternals { runCommand(command: string, args: string[], options?: { cwd?: string }): Promise; + getPackageManagerName(): string; + getGitDependencyInstallArgs(): string[]; runCommandCapture( command: string, args: string[], @@ -764,7 +766,7 @@ Content`, ); }); - it("should install git package dependencies with --omit=dev", async () => { + it("should install git package dependencies without auto-installing peers", async () => { const source = "git:github.com/user/repo"; const targetDir = join(agentDir, "git", "github.com", "user", "repo"); const runCommandSpy = vi @@ -779,7 +781,9 @@ Content`, await packageManager.install(source); - expect(runCommandSpy).toHaveBeenCalledWith("npm", ["install", "--omit=dev"], { cwd: targetDir }); + expect(runCommandSpy).toHaveBeenCalledWith("npm", ["install", "--omit=dev", "--legacy-peer-deps"], { + cwd: targetDir, + }); }); it("should remove a newly created checkout when git clone fails", async () => { @@ -843,7 +847,9 @@ Content`, cwd: targetDir, }); expect(runCommandSpy).toHaveBeenCalledWith("git", ["clean", "-fdx"], { cwd: targetDir }); - expect(runCommandSpy).toHaveBeenCalledWith("npm", ["install", "--omit=dev"], { cwd: targetDir }); + expect(runCommandSpy).toHaveBeenCalledWith("npm", ["install", "--omit=dev", "--legacy-peer-deps"], { + cwd: targetDir, + }); }); it("should reconcile an existing git checkout to its update target when installing without a ref", async () => { @@ -878,9 +884,32 @@ Content`, expect(runCommandSpy).toHaveBeenCalledWith("git", ["clean", "-fdx"], { cwd: targetDir }); }); - it("should use plain install for git package dependencies when npmCommand is configured", async () => { + it("should prefer the package manager after a separator over the outer executable", () => { + // Regression for #9863. settingsManager = SettingsManager.inMemory({ - npmCommand: ["pnpm"], + npmCommand: ["npm", "exec", "--", "pnpm"], + }); + packageManager = new DefaultPackageManager({ + cwd: tempDir, + agentDir, + settingsManager, + }); + + const managerWithInternals = packageManager as unknown as PackageManagerInternals; + expect(managerWithInternals.getPackageManagerName()).toBe("pnpm"); + expect(managerWithInternals.getGitDependencyInstallArgs()).toEqual([ + "install", + "--prod", + "--config.auto-install-peers=false", + "--config.strict-peer-dependencies=false", + "--config.strict-dep-builds=false", + ]); + }); + + it("should detect pnpm through a corepack wrapper without a separator", async () => { + // Regression for #9863. + settingsManager = SettingsManager.inMemory({ + npmCommand: ["corepack", "pnpm"], }); packageManager = new DefaultPackageManager({ cwd: tempDir, @@ -902,10 +931,50 @@ Content`, await packageManager.install(source); - expect(runCommandSpy).toHaveBeenCalledWith("pnpm", ["install"], { cwd: targetDir }); + expect(runCommandSpy).toHaveBeenCalledWith( + "corepack", + [ + "pnpm", + "install", + "--prod", + "--config.auto-install-peers=false", + "--config.strict-peer-dependencies=false", + "--config.strict-dep-builds=false", + ], + { cwd: targetDir }, + ); }); - it("should update git package dependencies with --omit=dev", async () => { + it("should disable peer installation for git package dependencies with bun", async () => { + settingsManager = SettingsManager.inMemory({ + npmCommand: ["bun"], + }); + packageManager = new DefaultPackageManager({ + cwd: tempDir, + agentDir, + settingsManager, + }); + + const source = "git:github.com/user/repo"; + const targetDir = join(agentDir, "git", "github.com", "user", "repo"); + const runCommandSpy = vi + .spyOn(packageManager as any, "runCommand") + .mockImplementation(async (...callArgs: unknown[]) => { + const [command, args] = callArgs as [string, string[]]; + if (command === "git" && args[0] === "clone") { + mkdirSync(targetDir, { recursive: true }); + writeFileSync(join(targetDir, "package.json"), JSON.stringify({ name: "repo", version: "1.0.0" })); + } + }); + + await packageManager.install(source); + + expect(runCommandSpy).toHaveBeenCalledWith("bun", ["install", "--omit=dev", "--omit=peer"], { + cwd: targetDir, + }); + }); + + it("should update git package dependencies without auto-installing peers", async () => { const source = "git:github.com/user/repo"; const targetDir = join(tempDir, ".pi", "git", "github.com", "user", "repo"); mkdirSync(targetDir, { recursive: true }); @@ -929,7 +998,9 @@ Content`, await packageManager.update(source); - expect(runCommandSpy).toHaveBeenCalledWith("npm", ["install", "--omit=dev"], { cwd: targetDir }); + expect(runCommandSpy).toHaveBeenCalledWith("npm", ["install", "--omit=dev", "--legacy-peer-deps"], { + cwd: targetDir, + }); }); it("should repair missing git package dependencies when the checkout is already current", async () => { @@ -954,7 +1025,9 @@ Content`, await packageManager.update(source); - expect(runCommandSpy).toHaveBeenCalledWith("npm", ["install", "--omit=dev"], { cwd: targetDir }); + expect(runCommandSpy).toHaveBeenCalledWith("npm", ["install", "--omit=dev", "--legacy-peer-deps"], { + cwd: targetDir, + }); expect(runCommandSpy).not.toHaveBeenCalledWith("git", ["clean", "-fdx"], { cwd: targetDir }); }); @@ -986,10 +1059,12 @@ Content`, await expect(packageManager.update(source)).rejects.toThrow("simulated clean failure"); - expect(runCommandSpy).toHaveBeenCalledWith("npm", ["install", "--omit=dev"], { cwd: targetDir }); + expect(runCommandSpy).toHaveBeenCalledWith("npm", ["install", "--omit=dev", "--legacy-peer-deps"], { + cwd: targetDir, + }); }); - it("should use plain install through npmCommand argv when updating git package dependencies", async () => { + it("should disable peer installation through wrapped pnpm when updating git dependencies", async () => { settingsManager = SettingsManager.inMemory({ npmCommand: ["mise", "exec", "node@20", "--", "pnpm"], }); @@ -1022,9 +1097,21 @@ Content`, await packageManager.update(source); - expect(runCommandSpy).toHaveBeenCalledWith("mise", ["exec", "node@20", "--", "pnpm", "install"], { - cwd: targetDir, - }); + expect(runCommandSpy).toHaveBeenCalledWith( + "mise", + [ + "exec", + "node@20", + "--", + "pnpm", + "install", + "--prod", + "--config.auto-install-peers=false", + "--config.strict-peer-dependencies=false", + "--config.strict-dep-builds=false", + ], + { cwd: targetDir }, + ); }); it("should use npmCommand argv for npm root lookup and invalidate cached root when npmCommand changes", () => { diff --git a/packages/coding-agent/test/resource-loader.test.ts b/packages/coding-agent/test/resource-loader.test.ts index 435bd5e22..ec726c960 100644 --- a/packages/coding-agent/test/resource-loader.test.ts +++ b/packages/coding-agent/test/resource-loader.test.ts @@ -40,6 +40,66 @@ describe("DefaultResourceLoader", () => { expect(loader.getThemes().themes).toEqual([]); }); + it("should not treat a project manifest as the owner of a project extension", async () => { + const extensionsDir = join(cwd, ".pi", "extensions"); + mkdirSync(extensionsDir, { recursive: true }); + writeFileSync( + join(cwd, "package.json"), + JSON.stringify({ dependencies: { "@earendil-works/pi-coding-agent": "1.0.0" } }), + ); + writeFileSync(join(extensionsDir, "project-extension.ts"), "export default function() {}"); + + const loader = new DefaultResourceLoader({ cwd, agentDir }); + await loader.reload(); + + expect(loader.getExtensions().extensions).toHaveLength(1); + expect(loader.getExtensions().warnings).toEqual([]); + }); + + it("should warn about host dependencies in an extension package manifest", async () => { + // Regression for #9863. + const packageRoot = join(tempDir, "extension-package"); + const extensionsDir = join(packageRoot, "extensions"); + mkdirSync(extensionsDir, { recursive: true }); + writeFileSync( + join(packageRoot, "package.json"), + JSON.stringify({ dependencies: { "@earendil-works/pi-coding-agent": "1.0.0" } }), + ); + writeFileSync(join(extensionsDir, "package-extension.ts"), "export default function() {}"); + + const loader = new DefaultResourceLoader({ + cwd, + agentDir, + settingsManager: SettingsManager.inMemory({ packages: [packageRoot] }), + }); + await loader.reload(); + + expect(loader.getExtensions().extensions).toHaveLength(1); + expect(loader.getExtensions().warnings).toEqual([ + { + path: join(packageRoot, "package.json"), + warning: + 'Host-provided extension packages must be declared in peerDependencies with a "*" range, not dependencies: @earendil-works/pi-coding-agent. Installed copies can bypass the extension loader and create duplicate runtime modules.', + }, + ]); + }); + + it("should fail when an extension package manifest cannot be parsed", async () => { + const packageRoot = join(tempDir, "invalid-extension-package"); + const extensionsDir = join(packageRoot, "extensions"); + mkdirSync(extensionsDir, { recursive: true }); + writeFileSync(join(packageRoot, "package.json"), "{"); + writeFileSync(join(extensionsDir, "package-extension.ts"), "export default function() {}"); + + const loader = new DefaultResourceLoader({ + cwd, + agentDir, + settingsManager: SettingsManager.inMemory({ packages: [packageRoot] }), + }); + + await expect(loader.reload()).rejects.toThrow(SyntaxError); + }); + it("should discover skills from agentDir", async () => { const skillsDir = join(agentDir, "skills"); mkdirSync(skillsDir, { recursive: true });