From 6a140371d6fa1069f89724741de912ab2f266ef8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 4 Aug 2026 23:11:21 +0200 Subject: [PATCH] fix(agent): handle Windows resource paths --- packages/agent/CHANGELOG.md | 4 ++ packages/agent/src/harness/env/nodejs.ts | 4 +- .../agent/src/harness/prompt-templates.ts | 13 ++---- packages/agent/src/harness/skills.ts | 40 +++++++++---------- 4 files changed, 30 insertions(+), 31 deletions(-) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 94384b4fb..1a34b021b 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -13,6 +13,10 @@ - Added bounded `Session.findEntriesOnBranch()` and `findEntryOnBranch()` queries with explicit traversal, filtering, ordering, and limit options. - Added a compile-complete `AgentHarness` v2 scaffold; unfinished operation paths reject with `HarnessNotImplemented` while durable execution is implemented. +### Fixed + +- Fixed Windows path handling for `NodeExecutionEnv` file basenames, recursive skill loading, and prompt template names. + ## [0.83.0] - 2026-07-29 ## [0.82.1] - 2026-07-25 diff --git a/packages/agent/src/harness/env/nodejs.ts b/packages/agent/src/harness/env/nodejs.ts index f9f94f8f9..05c667309 100644 --- a/packages/agent/src/harness/env/nodejs.ts +++ b/packages/agent/src/harness/env/nodejs.ts @@ -14,7 +14,7 @@ import { writeFile, } from "node:fs/promises"; import { homedir, tmpdir } from "node:os"; -import { isAbsolute, join, resolve } from "node:path"; +import { basename, isAbsolute, join, resolve } from "node:path"; import { createInterface } from "node:readline"; import { fileURLToPath } from "node:url"; import { @@ -81,7 +81,7 @@ function fileInfoFromStats( const kind = fileKindFromStats(stats); if (!kind) return err(new FileError("invalid", "Unsupported file type", path)); return ok({ - name: path.replace(/\/+$/, "").split("/").pop() ?? path, + name: basename(path), path, kind, size: stats.size, diff --git a/packages/agent/src/harness/prompt-templates.ts b/packages/agent/src/harness/prompt-templates.ts index 6c1b6be0b..52b5128c2 100644 --- a/packages/agent/src/harness/prompt-templates.ts +++ b/packages/agent/src/harness/prompt-templates.ts @@ -53,7 +53,7 @@ export async function loadPromptTemplates( promptTemplates.push(...result.promptTemplates); diagnostics.push(...result.diagnostics); } else if (kind === "file" && info.name.endsWith(".md")) { - const result = await loadTemplateFromFile(env, info.path); + const result = await loadTemplateFromFile(env, info.path, info.name); if (result.promptTemplate) promptTemplates.push(result.promptTemplate); diagnostics.push(...result.diagnostics); } @@ -113,7 +113,7 @@ async function loadTemplatesFromDir( for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) { const kind = await resolveKind(env, entry, diagnostics); if (kind !== "file" || !entry.name.endsWith(".md")) continue; - const result = await loadTemplateFromFile(env, entry.path); + const result = await loadTemplateFromFile(env, entry.path, entry.name); if (result.promptTemplate) promptTemplates.push(result.promptTemplate); diagnostics.push(...result.diagnostics); } @@ -123,6 +123,7 @@ async function loadTemplatesFromDir( async function loadTemplateFromFile( env: ExecutionEnv, filePath: string, + fileName: string, ): Promise<{ promptTemplate: PromptTemplate | null; diagnostics: PromptTemplateDiagnostic[] }> { const diagnostics: PromptTemplateDiagnostic[] = []; const rawContent = await env.readTextFile(filePath); @@ -156,7 +157,7 @@ async function loadTemplateFromFile( } return { promptTemplate: { - name: basenameEnvPath(filePath).replace(/\.md$/i, ""), + name: fileName.replace(/\.md$/i, ""), description, content: body, }, @@ -213,12 +214,6 @@ function parseFrontmatter>( } } -function basenameEnvPath(path: string): string { - const normalized = path.replace(/\/+$/, ""); - const slashIndex = normalized.lastIndexOf("/"); - return slashIndex === -1 ? normalized : normalized.slice(slashIndex + 1); -} - /** Parse an argument string using simple shell-style single and double quotes. */ export function parseCommandArgs(argsString: string): string[] { const args: string[] = []; diff --git a/packages/agent/src/harness/skills.ts b/packages/agent/src/harness/skills.ts index a1a4c0c4a..0c0b54330 100644 --- a/packages/agent/src/harness/skills.ts +++ b/packages/agent/src/harness/skills.ts @@ -142,7 +142,7 @@ async function loadSkillsFromDirInternal( const relPath = relativeEnvPath(rootDir, fullPath); if (ignoreMatcher.ignores(relPath)) continue; - const result = await loadSkillFromFile(env, fullPath); + const result = await loadSkillFromFile(env, fullPath, dirInfo.name); if (result.skill) skills.push(result.skill); diagnostics.push(...result.diagnostics); return { skills, diagnostics }; @@ -166,7 +166,7 @@ async function loadSkillsFromDirInternal( } if (kind !== "file" || !includeRootFiles || !entry.name.endsWith(".md")) continue; - const result = await loadSkillFromFile(env, fullPath); + const result = await loadSkillFromFile(env, fullPath, dirInfo.name); if (result.skill) skills.push(result.skill); diagnostics.push(...result.diagnostics); } @@ -185,7 +185,17 @@ async function addIgnoreRules( const prefix = relativeDir ? `${relativeDir}/` : ""; for (const filename of IGNORE_FILE_NAMES) { - const ignorePath = joinEnvPath(dir, filename); + const ignorePathResult = await env.joinPath([dir, filename]); + if (!ignorePathResult.ok) { + diagnostics.push({ + type: "warning", + code: "file_info_failed", + message: ignorePathResult.error.message, + path: dir, + }); + continue; + } + const ignorePath = ignorePathResult.value; const info = await env.fileInfo(ignorePath); if (!info.ok) { if (info.error.code !== "not_found") { @@ -233,6 +243,7 @@ function prefixIgnorePattern(line: string, prefix: string): string | null { async function loadSkillFromFile( env: ExecutionEnv, filePath: string, + parentDirName: string, ): Promise<{ skill: Skill | null; diagnostics: SkillDiagnostic[] }> { const diagnostics: SkillDiagnostic[] = []; const rawContent = await env.readTextFile(filePath); @@ -248,8 +259,6 @@ async function loadSkillFromFile( } const { frontmatter, body } = parsed.value; - const skillDir = dirnameEnvPath(filePath); - const parentDirName = basenameEnvPath(skillDir); const description = typeof frontmatter.description === "string" ? frontmatter.description : undefined; for (const error of validateDescription(description)) { @@ -349,25 +358,16 @@ async function resolveKind( return target.value.kind === "file" || target.value.kind === "directory" ? target.value.kind : undefined; } -function joinEnvPath(base: string, child: string): string { - return `${base.replace(/\/+$/, "")}/${child.replace(/^\/+/, "")}`; -} - function dirnameEnvPath(path: string): string { - const normalized = path.replace(/\/+$/, ""); - const slashIndex = normalized.lastIndexOf("/"); - return slashIndex <= 0 ? "/" : normalized.slice(0, slashIndex); -} - -function basenameEnvPath(path: string): string { - const normalized = path.replace(/\/+$/, ""); - const slashIndex = normalized.lastIndexOf("/"); - return slashIndex === -1 ? normalized : normalized.slice(slashIndex + 1); + const normalized = path.replace(/[\\/]+$/, ""); + const separatorIndex = Math.max(normalized.lastIndexOf("/"), normalized.lastIndexOf("\\")); + if (separatorIndex === 2 && normalized[1] === ":") return normalized.slice(0, 3); + return separatorIndex <= 0 ? "/" : normalized.slice(0, separatorIndex); } function relativeEnvPath(root: string, path: string): string { - const normalizedRoot = root.replace(/\/+$/, ""); - const normalizedPath = path.replace(/\/+$/, ""); + const normalizedRoot = root.replace(/\\/g, "/").replace(/\/+$/, ""); + const normalizedPath = path.replace(/\\/g, "/").replace(/\/+$/, ""); if (normalizedPath === normalizedRoot) return ""; return normalizedPath.startsWith(`${normalizedRoot}/`) ? normalizedPath.slice(normalizedRoot.length + 1)