fix(sessions): normalize package resource loading (#129098)

This commit is contained in:
Peter Steinberger
2026-08-25 02:27:20 -07:00
committed by GitHub
parent 10c6be319e
commit 315969f669
4 changed files with 262 additions and 340 deletions
+1 -1
View File
@@ -2028,7 +2028,7 @@ src/agents/sessions/extensions/types.ts 1
src/agents/sessions/keybindings.ts 2
src/agents/sessions/model-registry.ts 13
src/agents/sessions/model-resolver.ts 1
src/agents/sessions/package-manager.ts 8
src/agents/sessions/package-manager.ts 3
src/agents/sessions/resolve-config-value.ts 1
src/agents/sessions/sdk.ts 3
src/agents/sessions/session-manager-branching.ts 1
+59 -1
View File
@@ -2,7 +2,7 @@
// project, and npm-declared agent resources.
import { mkdir, stat, symlink, writeFile } from "node:fs/promises";
import { join, relative } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js";
import { withEnvAsync } from "../../test-utils/env.js";
import { DefaultPackageManager } from "./package-manager.js";
@@ -284,6 +284,64 @@ describe("DefaultPackageManager", () => {
expect(resolved.themes).toEqual([]);
});
it("honors filters on direct local extension files", async () => {
const root = tempDirs.make("openclaw-package-manager-filter-");
const extensionPath = join(root, "extension.ts");
await writeFile(extensionPath, "export default {};\n", "utf-8");
const manager = new DefaultPackageManager({
cwd: root,
agentDir: join(root, "agent"),
settingsManager: SettingsManager.inMemory({
packages: [{ source: extensionPath, extensions: [] }],
}),
});
expect((await manager.resolve()).extensions).toEqual([
expect.objectContaining({ path: extensionPath, enabled: false }),
]);
});
it("treats object local directories without filters like string sources", async () => {
const root = tempDirs.make("openclaw-package-manager-object-");
const extensionDir = join(root, "extension");
const extensionPath = join(extensionDir, "index.ts");
await mkdir(extensionDir);
await writeFile(extensionPath, "export default {};\n", "utf-8");
const resolveSource = async (source: string | { source: string; extensions?: string[] }) =>
await new DefaultPackageManager({
cwd: root,
agentDir: join(root, "agent"),
settingsManager: SettingsManager.inMemory({ packages: [source] }),
}).resolve();
expect(await resolveSource({ source: extensionDir })).toEqual(
await resolveSource(extensionDir),
);
expect((await resolveSource({ source: extensionDir, extensions: [] })).extensions).toEqual([
expect.objectContaining({ path: extensionDir, enabled: false }),
]);
});
it.each([
["local", "./missing-extension.ts"],
["npm", "npm:@openclaw/missing-test"],
["git", "https://github.com/openclaw/missing-test.git"],
])("reports missing %s package sources through the owner callback", async (_kind, source) => {
const root = tempDirs.make("openclaw-package-manager-missing-");
const onMissing = vi.fn(async () => "skip" as const);
const manager = new DefaultPackageManager({
cwd: root,
agentDir: join(root, "agent"),
settingsManager: SettingsManager.inMemory({ packages: [source] }),
});
const resolved = await manager.resolve(onMissing);
expect(onMissing).toHaveBeenCalledOnce();
expect(onMissing).toHaveBeenCalledWith(source);
expect(resolved).toEqual({ extensions: [], skills: [], prompts: [], themes: [] });
});
it("keeps temporary package paths in a private per-agent directory", async () => {
const root = tempDirs.make("openclaw-package-manager-temp-");
const agentDir = join(root, "agent");
+180 -338
View File
@@ -88,13 +88,6 @@ interface ResourceManifest {
themes?: string[];
}
interface ResourceAccumulator {
extensions: Map<string, { metadata: PathMetadata; enabled: boolean }>;
skills: Map<string, { metadata: PathMetadata; enabled: boolean }>;
prompts: Map<string, { metadata: PathMetadata; enabled: boolean }>;
themes: Map<string, { metadata: PathMetadata; enabled: boolean }>;
}
/**
* Compute a numeric precedence rank for a resource based on its metadata.
* Lower rank = higher precedence. Used to sort resolved resources so that
@@ -124,8 +117,14 @@ interface PackageFilter {
type ResourceType = "extensions" | "skills" | "prompts" | "themes";
type TopLevelAutoResourceType = Extract<ResourceType, "prompts" | "themes">;
type ResourceState = { metadata: PathMetadata; enabled: boolean };
type ResourceAccumulator = Record<ResourceType, Map<string, ResourceState>>;
const RESOURCE_TYPES: ResourceType[] = ["extensions", "skills", "prompts", "themes"];
const RESOURCE_TYPES = ["extensions", "skills", "prompts", "themes"] as const;
type ResolvedPackageTarget =
| { kind: "missing" }
| { kind: "file" | "directory"; path: string; baseDir: string };
const FILE_PATTERNS: Record<ResourceType, RegExp> = {
extensions: /\.(ts|js)$/,
@@ -543,6 +542,13 @@ function collectResourceFiles(dir: string, resourceType: ResourceType): string[]
return collectFiles(dir, FILE_PATTERNS[resourceType]);
}
const AUTO_RESOURCE_COLLECTORS = {
extensions: collectAutoExtensionEntries,
skills: (dir: string) => collectAutoSkillEntries(dir, "openclaw"),
prompts: (dir: string) => collectTopLevelAutoResourceEntries(dir, "prompts"),
themes: (dir: string) => collectTopLevelAutoResourceEntries(dir, "themes"),
} satisfies Record<ResourceType, (dir: string) => string[]>;
function resolveRealPathIfPossible(path: string): string {
try {
return realpathSync.native(path);
@@ -658,6 +664,13 @@ function applyPatterns(allPaths: string[], patterns: string[], baseDir: string):
return new Set(result);
}
function getPackageFilter(pkg: PackageSource): PackageFilter | undefined {
if (typeof pkg === "string") {
return undefined;
}
return RESOURCE_TYPES.some((resourceType) => pkg[resourceType] !== undefined) ? pkg : undefined;
}
export class DefaultPackageManager implements PackageManager {
private cwd: string;
private agentDir: string;
@@ -692,32 +705,20 @@ export class DefaultPackageManager implements PackageManager {
const globalBaseDir = this.agentDir;
const projectBaseDir = join(this.cwd, CONFIG_DIR_NAME);
const localScopes = [
{ scope: "project", settings: projectSettings, baseDir: projectBaseDir },
{ scope: "user", settings: globalSettings, baseDir: globalBaseDir },
] as const;
for (const resourceType of RESOURCE_TYPES) {
const target = this.getTargetMap(accumulator, resourceType);
const globalEntries = globalSettings[resourceType] ?? [];
const projectEntries = projectSettings[resourceType] ?? [];
this.resolveLocalEntries(
projectEntries,
resourceType,
target,
{
source: "local",
scope: "project",
origin: "top-level",
},
projectBaseDir,
);
this.resolveLocalEntries(
globalEntries,
resourceType,
target,
{
source: "local",
scope: "user",
origin: "top-level",
},
globalBaseDir,
);
for (const { scope, settings, baseDir } of localScopes) {
this.resolveLocalEntries(
settings[resourceType] ?? [],
resourceType,
accumulator[resourceType],
{ source: "local", scope, origin: "top-level" },
baseDir,
);
}
}
this.addAutoDiscoveredResources(
@@ -753,79 +754,89 @@ export class DefaultPackageManager implements PackageManager {
): Promise<void> {
for (const { pkg, scope } of sources) {
const sourceStr = typeof pkg === "string" ? pkg : pkg.source;
const filter = typeof pkg === "object" ? pkg : undefined;
const filter = getPackageFilter(pkg);
const parsed = this.parseSource(sourceStr);
const metadata: PathMetadata = { source: sourceStr, scope, origin: "package" };
if (parsed.type === "local") {
const baseDir = this.getBaseDirForScope(scope);
this.resolveLocalExtensionSource(parsed, accumulator, filter, metadata, baseDir);
const target = this.resolvePackageTarget(parsed, scope);
if (!target) {
continue;
}
const handleMissing = async (): Promise<void> => {
if (!onMissing) {
return;
}
const action = await onMissing(sourceStr);
if (action === "error") {
if (target.kind === "missing") {
if (onMissing && (await onMissing(sourceStr)) === "error") {
throw new Error(`Missing source: ${sourceStr}`);
}
};
if (parsed.type === "npm") {
const installedPath = this.getNpmInstallPath(parsed, scope);
const missingOrWrongVersion =
!existsSync(installedPath) ||
(parsed.pinned && !this.installedNpmMatchesPinnedVersion(parsed, installedPath));
if (missingOrWrongVersion) {
await handleMissing();
continue;
}
metadata.baseDir = installedPath;
this.collectPackageResources(installedPath, accumulator, filter, metadata);
continue;
}
if (parsed.type === "git") {
const installedPath = this.getGitInstallPath(parsed, scope);
if (!existsSync(installedPath)) {
await handleMissing();
continue;
}
metadata.baseDir = installedPath;
this.collectPackageResources(installedPath, accumulator, filter, metadata);
metadata.baseDir = target.baseDir;
if (target.kind === "file") {
this.addResource(
accumulator.extensions,
target.path,
metadata,
this.isExtensionEnabled(target.path, filter?.extensions, target.baseDir),
);
continue;
}
const hasPackageLayout = this.collectPackageResources(
target.path,
accumulator,
filter,
metadata,
);
if (parsed.type === "local" && !hasPackageLayout) {
this.addResource(
accumulator.extensions,
target.path,
metadata,
this.isExtensionEnabled(target.path, filter?.extensions, target.baseDir),
);
}
}
}
private resolveLocalExtensionSource(
source: LocalSource,
accumulator: ResourceAccumulator,
filter: PackageFilter | undefined,
metadata: PathMetadata,
baseDir: string,
): void {
const resolved = this.resolvePathFromBase(source.path, baseDir);
if (!existsSync(resolved)) {
return;
private resolvePackageTarget(
source: ParsedSource,
scope: SourceScope,
): ResolvedPackageTarget | undefined {
if (source.type === "npm") {
const path = this.getNpmInstallPath(source, scope);
return !existsSync(path) ||
(source.pinned && !this.installedNpmMatchesPinnedVersion(source, path))
? { kind: "missing" }
: { kind: "directory", path, baseDir: path };
}
if (source.type === "git") {
const path = this.getGitInstallPath(source, scope);
return existsSync(path) ? { kind: "directory", path, baseDir: path } : { kind: "missing" };
}
const path = this.resolvePathFromBase(source.path, this.getBaseDirForScope(scope));
if (!existsSync(path)) {
return { kind: "missing" };
}
try {
const stats = statSync(resolved);
const stats = statSync(path);
if (stats.isFile()) {
metadata.baseDir = dirname(resolved);
this.addResource(accumulator.extensions, resolved, metadata, true);
return;
return { kind: "file", path, baseDir: dirname(path) };
}
if (stats.isDirectory()) {
metadata.baseDir = resolved;
const resources = this.collectPackageResources(resolved, accumulator, filter, metadata);
if (!resources) {
this.addResource(accumulator.extensions, resolved, metadata, true);
}
return { kind: "directory", path, baseDir: path };
}
} catch {}
return undefined;
}
private isExtensionEnabled(
path: string,
patterns: string[] | undefined,
baseDir: string,
): boolean {
if (patterns === undefined) {
return true;
}
return patterns.length > 0 && applyPatterns([path], patterns, baseDir).has(path);
}
private parseSource(source: string): ParsedSource {
@@ -1012,69 +1023,29 @@ export class DefaultPackageManager implements PackageManager {
filter: PackageFilter | undefined,
metadata: PathMetadata,
): boolean {
if (filter) {
for (const resourceType of RESOURCE_TYPES) {
const patterns = filter[resourceType as keyof PackageFilter];
const target = this.getTargetMap(accumulator, resourceType);
if (patterns !== undefined) {
this.applyPackageFilter(packageRoot, patterns, resourceType, target, metadata);
} else {
this.collectDefaultResources(packageRoot, resourceType, target, metadata);
}
}
return true;
}
const manifest = this.readResourceManifest(packageRoot);
if (manifest) {
for (const resourceType of RESOURCE_TYPES) {
const entries = manifest[resourceType as keyof ResourceManifest];
this.addManifestEntries(
entries,
packageRoot,
resourceType,
this.getTargetMap(accumulator, resourceType),
metadata,
);
}
return true;
}
let hasAnyDir = false;
const manifest = readResourceManifestFile(join(packageRoot, "package.json"));
const hasPackageLayout =
manifest !== null || RESOURCE_TYPES.some((type) => existsSync(join(packageRoot, type)));
for (const resourceType of RESOURCE_TYPES) {
const patterns = filter?.[resourceType];
const target = accumulator[resourceType];
if (patterns !== undefined) {
this.applyPackageFilter(packageRoot, patterns, resourceType, target, metadata);
continue;
}
const entries = manifest?.[resourceType];
if (manifest !== null && (!filter || entries !== undefined)) {
this.addManifestEntries(entries, packageRoot, resourceType, target, metadata);
continue;
}
const dir = join(packageRoot, resourceType);
if (existsSync(dir)) {
// Collect all files from the directory (all enabled by default)
const files = this.collectConventionResourceFiles(packageRoot, resourceType);
for (const f of files) {
this.addResource(this.getTargetMap(accumulator, resourceType), f, metadata, true);
for (const path of this.collectConventionResourceFiles(packageRoot, resourceType)) {
this.addResource(target, path, metadata, true);
}
hasAnyDir = true;
}
}
return hasAnyDir;
}
private collectDefaultResources(
packageRoot: string,
resourceType: ResourceType,
target: Map<string, { metadata: PathMetadata; enabled: boolean }>,
metadata: PathMetadata,
): void {
const manifest = this.readResourceManifest(packageRoot);
const entries = manifest?.[resourceType as keyof ResourceManifest];
if (entries) {
this.addManifestEntries(entries, packageRoot, resourceType, target, metadata);
return;
}
const dir = join(packageRoot, resourceType);
if (existsSync(dir)) {
// Collect all files from the directory (all enabled by default)
const files = this.collectConventionResourceFiles(packageRoot, resourceType);
for (const f of files) {
this.addResource(target, f, metadata, true);
}
}
return hasPackageLayout;
}
private applyPackageFilter(
@@ -1084,7 +1055,7 @@ export class DefaultPackageManager implements PackageManager {
target: Map<string, { metadata: PathMetadata; enabled: boolean }>,
metadata: PathMetadata,
): void {
const { allFiles } = this.collectManifestFiles(packageRoot, resourceType);
const allFiles = this.collectManifestFiles(packageRoot, resourceType);
if (userPatterns.length === 0) {
// Empty array explicitly disables all resources of this type
@@ -1103,29 +1074,20 @@ export class DefaultPackageManager implements PackageManager {
}
}
/**
* Collect all files from a package for a resource type, applying manifest patterns.
* Returns { allFiles, enabledByManifest } where enabledByManifest is the set of files
* that pass the manifest's own patterns.
*/
private collectManifestFiles(
packageRoot: string,
resourceType: ResourceType,
): { allFiles: string[]; enabledByManifest: Set<string> } {
const manifest = this.readResourceManifest(packageRoot);
const entries = manifest?.[resourceType as keyof ResourceManifest];
private collectManifestFiles(packageRoot: string, resourceType: ResourceType): string[] {
const manifest = readResourceManifestFile(join(packageRoot, "package.json"));
const entries = manifest?.[resourceType];
if (entries && entries.length > 0) {
const allFiles = this.collectFilesFromManifestEntries(entries, packageRoot, resourceType);
const manifestPatterns = entries.filter(isOverridePattern);
const enabledByManifest =
return Array.from(
manifestPatterns.length > 0
? applyPatterns(allFiles, manifestPatterns, packageRoot)
: new Set(allFiles);
return { allFiles: Array.from(enabledByManifest), enabledByManifest };
: new Set(allFiles),
);
}
const allFiles = this.collectConventionResourceFiles(packageRoot, resourceType);
return { allFiles, enabledByManifest: new Set(allFiles) };
return this.collectConventionResourceFiles(packageRoot, resourceType);
}
private collectConventionResourceFiles(
@@ -1142,21 +1104,6 @@ export class DefaultPackageManager implements PackageManager {
);
}
private readResourceManifest(packageRoot: string): ResourceManifest | null {
const packageJsonPath = join(packageRoot, "package.json");
if (!existsSync(packageJsonPath)) {
return null;
}
try {
const content = readFileSync(packageJsonPath, "utf-8");
const pkg = JSON.parse(content) as { openclaw?: ResourceManifest };
return pkg.openclaw ?? null;
} catch {
return null;
}
}
private addManifestEntries(
entries: string[] | undefined,
root: string,
@@ -1244,160 +1191,73 @@ export class DefaultPackageManager implements PackageManager {
globalBaseDir: string,
projectBaseDir: string,
): void {
const userMetadata: PathMetadata = {
source: "auto",
scope: "user",
origin: "top-level",
baseDir: globalBaseDir,
};
const projectMetadata: PathMetadata = {
source: "auto",
scope: "project",
origin: "top-level",
baseDir: projectBaseDir,
};
const userOverrides = {
extensions: globalSettings.extensions ?? [],
skills: globalSettings.skills ?? [],
prompts: globalSettings.prompts ?? [],
themes: globalSettings.themes ?? [],
};
const projectOverrides = {
extensions: projectSettings.extensions ?? [],
skills: projectSettings.skills ?? [],
prompts: projectSettings.prompts ?? [],
themes: projectSettings.themes ?? [],
};
const userDirs = {
extensions: join(globalBaseDir, "extensions"),
skills: join(globalBaseDir, "skills"),
prompts: join(globalBaseDir, "prompts"),
themes: join(globalBaseDir, "themes"),
};
const projectDirs = {
extensions: join(projectBaseDir, "extensions"),
skills: join(projectBaseDir, "skills"),
prompts: join(projectBaseDir, "prompts"),
themes: join(projectBaseDir, "themes"),
};
const userAgentsSkillsDir = join(getHomeDir(), ".agents", "skills");
const projectAgentsSkillDirs = collectAncestorAgentsSkillDirs(this.cwd).filter(
(dir) => resolve(dir) !== resolve(userAgentsSkillsDir),
);
const addScopeResources = (options: {
scope: Extract<SourceScope, "project" | "user">;
settings: ReturnType<SettingsManager["getGlobalSettings"]>;
baseDir: string;
agentsSkillDirs: string[];
}): void => {
const metadata: PathMetadata = {
source: "auto",
scope: options.scope,
origin: "top-level",
baseDir: options.baseDir,
};
const addResources = (
resourceType: ResourceType,
paths: string[],
resourceMetadata = metadata,
patternBaseDir = options.baseDir,
): void => {
for (const path of paths) {
this.addResource(
accumulator[resourceType],
path,
resourceMetadata,
isEnabledByOverrides(path, options.settings[resourceType] ?? [], patternBaseDir),
);
}
};
const addResources = (
resourceType: ResourceType,
paths: string[],
metadata: PathMetadata,
overrides: string[],
baseDir: string,
) => {
const target = this.getTargetMap(accumulator, resourceType);
for (const path of paths) {
const enabled = isEnabledByOverrides(path, overrides, baseDir);
this.addResource(target, path, metadata, enabled);
for (const resourceType of RESOURCE_TYPES) {
addResources(
resourceType,
AUTO_RESOURCE_COLLECTORS[resourceType](join(options.baseDir, resourceType)),
);
if (resourceType !== "skills") {
continue;
}
// .agents roots use different traversal and pattern bases; flattening them
// into the ordinary skill directory would weaken containment and overrides.
for (const skillsDir of options.agentsSkillDirs) {
const agentsBaseDir = dirname(skillsDir);
addResources(
"skills",
collectAutoSkillEntries(skillsDir, "agents"),
{ ...metadata, baseDir: agentsBaseDir },
agentsBaseDir,
);
}
}
};
// Project extensions from the embedded agent project directory.
addResources(
"extensions",
collectAutoExtensionEntries(projectDirs.extensions),
projectMetadata,
projectOverrides.extensions,
projectBaseDir,
);
// Project skills from the embedded agent project directory.
addResources(
"skills",
collectAutoSkillEntries(projectDirs.skills, "openclaw"),
projectMetadata,
projectOverrides.skills,
projectBaseDir,
);
// Project skills from .agents/ (each with its own baseDir)
for (const agentsSkillsDir of projectAgentsSkillDirs) {
const agentsBaseDir = dirname(agentsSkillsDir); // the .agents directory
const agentsMetadata: PathMetadata = {
...projectMetadata,
baseDir: agentsBaseDir,
};
addResources(
"skills",
collectAutoSkillEntries(agentsSkillsDir, "agents"),
agentsMetadata,
projectOverrides.skills,
agentsBaseDir,
);
}
addResources(
"prompts",
collectTopLevelAutoResourceEntries(projectDirs.prompts, "prompts"),
projectMetadata,
projectOverrides.prompts,
projectBaseDir,
);
addResources(
"themes",
collectTopLevelAutoResourceEntries(projectDirs.themes, "themes"),
projectMetadata,
projectOverrides.themes,
projectBaseDir,
);
// User extensions from ~/.openclaw/agent/
addResources(
"extensions",
collectAutoExtensionEntries(userDirs.extensions),
userMetadata,
userOverrides.extensions,
globalBaseDir,
);
// User skills from ~/.openclaw/agent/
addResources(
"skills",
collectAutoSkillEntries(userDirs.skills, "openclaw"),
userMetadata,
userOverrides.skills,
globalBaseDir,
);
if (isDefaultStateDir()) {
// Home-scoped personal skills belong to the default install, not isolated state roots.
const userAgentsBaseDir = dirname(userAgentsSkillsDir);
const userAgentsMetadata: PathMetadata = {
...userMetadata,
baseDir: userAgentsBaseDir,
};
addResources(
"skills",
collectAutoSkillEntries(userAgentsSkillsDir, "agents"),
userAgentsMetadata,
userOverrides.skills,
userAgentsBaseDir,
);
}
addResources(
"prompts",
collectTopLevelAutoResourceEntries(userDirs.prompts, "prompts"),
userMetadata,
userOverrides.prompts,
globalBaseDir,
);
addResources(
"themes",
collectTopLevelAutoResourceEntries(userDirs.themes, "themes"),
userMetadata,
userOverrides.themes,
globalBaseDir,
);
addScopeResources({
scope: "project",
settings: projectSettings,
baseDir: projectBaseDir,
agentsSkillDirs: projectAgentsSkillDirs,
});
addScopeResources({
scope: "user",
settings: globalSettings,
baseDir: globalBaseDir,
// Home-scoped personal skills belong only to the default install.
agentsSkillDirs: isDefaultStateDir() ? [userAgentsSkillsDir] : [],
});
}
private collectFilesFromPaths(paths: string[], resourceType: ResourceType): string[] {
@@ -1421,24 +1281,6 @@ export class DefaultPackageManager implements PackageManager {
return files;
}
private getTargetMap(
accumulator: ResourceAccumulator,
resourceType: ResourceType,
): Map<string, { metadata: PathMetadata; enabled: boolean }> {
switch (resourceType) {
case "extensions":
return accumulator.extensions;
case "skills":
return accumulator.skills;
case "prompts":
return accumulator.prompts;
case "themes":
return accumulator.themes;
default:
throw new Error(`Unknown resource type: ${String(resourceType)}`);
}
}
private addResource(
map: Map<string, { metadata: PathMetadata; enabled: boolean }>,
path: string,
@@ -6,6 +6,7 @@ import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js"
import { clearExtensionCache } from "./extensions/loader.js";
import { DefaultPackageManager } from "./package-manager.js";
import { DefaultResourceLoader } from "./resource-loader.js";
import { SettingsManager } from "./settings-manager.js";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
@@ -41,6 +42,27 @@ afterEach(() => {
});
describe("DefaultResourceLoader", () => {
it("does not load a direct local extension disabled by its package filter", async () => {
const root = tempDirs.make("openclaw-resource-loader-filter-");
const extensionPath = join(root, "extension.ts");
await writeFile(extensionPath, "export default function extension() {}\n");
const loader = new DefaultResourceLoader({
cwd: root,
agentDir: join(root, "agent"),
settingsManager: SettingsManager.inMemory({
packages: [{ source: extensionPath, extensions: [] }],
}),
noSkills: true,
noPromptTemplates: true,
noThemes: true,
noContextFiles: true,
});
await loader.reload();
expect(loader.getExtensions().extensions).toEqual([]);
});
it("skips ambient package resolution while preserving explicit resource paths", async () => {
const root = tempDirs.make("openclaw-resource-loader-explicit-");
const promptDir = join(root, "explicit-prompts");