diff --git a/src/agents/workspace-templates.test.ts b/src/agents/workspace-templates.test.ts index 1da24828792..1495744f94f 100644 --- a/src/agents/workspace-templates.test.ts +++ b/src/agents/workspace-templates.test.ts @@ -51,4 +51,27 @@ describe("resolveWorkspaceTemplateDir", () => { const resolved = await resolveWorkspaceTemplateDir({ cwd: distDir, moduleUrl }); expect(path.normalize(resolved)).toBe(path.resolve("docs", "reference", "templates")); }); + + it("resolves via module-relative ../docs fallback when packageRoot is null and cwd lacks templates", async () => { + // Simulates the bundled dist/ layout where ../../ overshoots but ../ is correct. + // When resolveOpenClawPackageRoot returns null (no package.json in ancestors), + // the ../docs/reference/templates fallback relative to the module should work. + const root = await makeTempRoot(); + // No package.json — packageRoot will be null + + const distDir = path.join(root, "dist"); + await fs.mkdir(distDir, { recursive: true }); + + // Create templates at ../docs (one level up from dist/) — the bundled layout + const templatesDir = path.join(root, "docs", "reference", "templates"); + await fs.mkdir(templatesDir, { recursive: true }); + await fs.writeFile(path.join(templatesDir, "AGENTS.md"), "# ok\n"); + + const moduleUrl = pathToFileURL(path.join(distDir, "workspace-XXXX.mjs")).toString(); + // Use a cwd that does NOT have docs/reference/templates + const fakeCwd = await makeTempRoot(); + + const resolved = await resolveWorkspaceTemplateDir({ cwd: fakeCwd, moduleUrl }); + expect(resolved).toBe(templatesDir); + }); }); diff --git a/src/agents/workspace-templates.ts b/src/agents/workspace-templates.ts index 11d733fa92c..d5032280523 100644 --- a/src/agents/workspace-templates.ts +++ b/src/agents/workspace-templates.ts @@ -3,10 +3,25 @@ import { fileURLToPath } from "node:url"; import { resolveOpenClawPackageRoot } from "../infra/openclaw-root.js"; import { pathExists } from "../utils.js"; -const FALLBACK_TEMPLATE_DIR = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - "../../docs/reference/templates", -); +/** + * Compute fallback template directory candidates relative to a module URL. + * + * In the source tree the module lives at `src/agents/workspace-templates.ts`, + * so `../../docs/reference/templates` resolves correctly to the repo root. + * + * After bundling, all files land in `dist/` (one level deep inside the + * package root), so `../../docs/reference/templates` overshoots by one + * directory (lands in node_modules/ instead of the package). We include + * both `../../` (source) and `../` (bundled dist) as fallback candidates + * to handle either layout. + */ +function computeFallbackTemplateDirs(moduleUrl: string): string[] { + const moduleDir = path.dirname(fileURLToPath(moduleUrl)); + return [ + path.resolve(moduleDir, "../../docs/reference/templates"), // source layout (src/agents/) + path.resolve(moduleDir, "../docs/reference/templates"), // bundled layout (dist/) + ]; +} let cachedTemplateDir: string | undefined; let resolvingTemplateDir: Promise | undefined; @@ -29,10 +44,14 @@ export async function resolveWorkspaceTemplateDir(opts?: { const cwd = opts?.cwd ?? process.cwd(); const packageRoot = await resolveOpenClawPackageRoot({ moduleUrl, argv1, cwd }); + const fallbackDirs = computeFallbackTemplateDirs(moduleUrl); const candidates = [ + // Preferred: resolved package root (most reliable) packageRoot ? path.join(packageRoot, "docs", "reference", "templates") : null, + // Fallback: relative to module file (handles both source and bundled layouts) + ...fallbackDirs, + // Last resort: relative to cwd (only useful if running from repo checkout) cwd ? path.resolve(cwd, "docs", "reference", "templates") : null, - FALLBACK_TEMPLATE_DIR, ].filter(Boolean) as string[]; for (const candidate of candidates) { @@ -42,7 +61,9 @@ export async function resolveWorkspaceTemplateDir(opts?: { } } - cachedTemplateDir = candidates[0] ?? FALLBACK_TEMPLATE_DIR; + // No candidate exists — return the package-root-based path (or first + // fallback) so the downstream error message shows a meaningful path. + cachedTemplateDir = candidates[0] ?? fallbackDirs[0]; return cachedTemplateDir; })();