fix(agent): handle Windows resource paths

This commit is contained in:
Armin Ronacher 2026-08-04 23:11:21 +02:00
parent 7830975233
commit 6a140371d6
4 changed files with 30 additions and 31 deletions

View file

@ -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

View file

@ -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,

View file

@ -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<T extends Record<string, unknown>>(
}
}
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[] = [];

View file

@ -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)