From 547b7e0db1ef7ead1adbfcb06a774fad878e62f1 Mon Sep 17 00:00:00 2001 From: lavi-kj Date: Tue, 28 Jul 2026 20:16:52 +0900 Subject: [PATCH 1/2] fix(coding-agent): terminate vendored rules findProjectRoot walk at non-cwd drive roots On Windows, resolve("/") is the cwd drive's root, but dirname() of another drive's root returns itself. For targets on a different drive than cwd (or UNC shares), the vendored pi-rules findProjectRoot marker walk never reaches filesystemRoot and spins forever - a synchronous infinite loop in the rules extension's tool_result handler that freezes the agent session (100% CPU, abort signal never processed) on every read/edit/write call targeting such paths. Stop the walk when dirname() stops progressing, document the adaptation in the extension's changes.md, and add a cross-drive regression test. Upstream fix for the vendored source: code-yeongyu/pi-rules#19 --- .../core/extensions/builtin/rules/changes.md | 3 +- .../builtin/rules/rules/project-root.ts | 9 +++++- ...ules-find-project-root-cross-drive.test.ts | 32 +++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 packages/coding-agent/test/suite/regressions/0000-rules-find-project-root-cross-drive.test.ts diff --git a/packages/coding-agent/src/core/extensions/builtin/rules/changes.md b/packages/coding-agent/src/core/extensions/builtin/rules/changes.md index 85e797ee2e..d2c10a8886 100644 --- a/packages/coding-agent/src/core/extensions/builtin/rules/changes.md +++ b/packages/coding-agent/src/core/extensions/builtin/rules/changes.md @@ -7,7 +7,8 @@ Vendored from [`code-yeongyu/pi-rules`](https://github.com/code-yeongyu/pi-rules - Imports rewritten by `scripts/vendor-transform.mjs`: `@mariozechner/pi-tui` -> `@earendil-works/pi-tui`; `@mariozechner/pi-coding-agent` symbols -> `../../types.ts` (and `Theme` -> `modes/interactive/theme/theme.ts`); relative `.js` import suffixes -> `.ts`. - `ui/dynamic-border.ts` and `ui/rules-banner.ts`: constructor parameter properties (`private readonly …`) -> explicit fields + constructor assignment (senpi's root tsconfig is `erasableSyntaxOnly`; parameter properties are disallowed). - Runtime dep `picomatch` (+ `@types/picomatch`) added to `package.json`. -- No behavior changes. Registers `/rules` and `/reload-rules` and discovers rule files from `.sisyphus/rules`, `.claude/rules`, `.cursor/rules`, `.github/instructions`, `AGENTS.md`, `CLAUDE.md`. +- `rules/project-root.ts`: `findProjectRoot` stops the marker walk when `dirname()` stops progressing, fixing an infinite synchronous loop for targets on a different Windows drive than cwd (or UNC shares). Behavior fix also submitted upstream: https://github.com/code-yeongyu/pi-rules/pull/19 — drop this adaptation once a fixed pi-rules release is re-vendored. +- No other behavior changes. Registers `/rules` and `/reload-rules` and discovers rule files from `.sisyphus/rules`, `.claude/rules`, `.cursor/rules`, `.github/instructions`, `AGENTS.md`, `CLAUDE.md`. ## Conflict zones diff --git a/packages/coding-agent/src/core/extensions/builtin/rules/rules/project-root.ts b/packages/coding-agent/src/core/extensions/builtin/rules/rules/project-root.ts index 2c3f387309..bf2c4decc4 100644 --- a/packages/coding-agent/src/core/extensions/builtin/rules/rules/project-root.ts +++ b/packages/coding-agent/src/core/extensions/builtin/rules/rules/project-root.ts @@ -25,6 +25,13 @@ export function findProjectRoot(startPath: string, markers: ReadonlyArray { + it("#given startPath on a different drive than cwd (Windows cross-drive) #when finding root #then terminates with null instead of looping forever", () => { + // Cross-drive paths only exist on Windows; this scenario cannot occur elsewhere. + if (process.platform !== "win32") { + return; + } + // The bug needs the temp dir and cwd on different drive roots; skip the body on single-drive machines. + if (parse(tmpdir()).root.toLowerCase() === parse(process.cwd()).root.toLowerCase()) { + return; + } + // given + const root = mkdtempSync(join(tmpdir(), "senpi-rules-project-root-")); + const startPath = join(root, "plain", "nested"); + mkdirSync(startPath, { recursive: true }); + + try { + // when + const result = findProjectRoot(startPath, [".nonexistent-marker-senpi"]); + + // then + expect(result).toBeNull(); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +}); From 558123aa3846f11638a8217737f5ee8fe9064504 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 30 Jul 2026 11:31:50 +0900 Subject: [PATCH 2/2] fix(coding-agent): exercise cross-drive regression in CI --- ...ules-find-project-root-cross-drive.test.ts | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/packages/coding-agent/test/suite/regressions/0000-rules-find-project-root-cross-drive.test.ts b/packages/coding-agent/test/suite/regressions/0000-rules-find-project-root-cross-drive.test.ts index ce41c3653f..a15f8567c6 100644 --- a/packages/coding-agent/test/suite/regressions/0000-rules-find-project-root-cross-drive.test.ts +++ b/packages/coding-agent/test/suite/regressions/0000-rules-find-project-root-cross-drive.test.ts @@ -1,32 +1,36 @@ -import { mkdirSync, mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join, parse } from "node:path"; -import { describe, expect, it } from "vitest"; +import { win32 } from "node:path"; +import { describe, expect, it, vi } from "vitest"; import { findProjectRoot } from "../../../src/core/extensions/builtin/rules/rules/project-root.ts"; +const { cwdRoot, startPath } = vi.hoisted(() => ({ + cwdRoot: "E:\\", + startPath: "C:\\workspace\\plain\\nested", +})); + +vi.mock("node:fs", () => ({ + existsSync: (path: string) => path === startPath, + statSync: () => ({ isDirectory: () => true }), +})); + +vi.mock("node:path", async (importOriginal) => { + const path = await importOriginal(); + return { + ...path, + dirname: path.win32.dirname, + join: path.win32.join, + resolve: (input: string) => (input === "/" ? cwdRoot : path.win32.resolve(input)), + }; +}); + describe("rules findProjectRoot cross-drive termination", () => { it("#given startPath on a different drive than cwd (Windows cross-drive) #when finding root #then terminates with null instead of looping forever", () => { - // Cross-drive paths only exist on Windows; this scenario cannot occur elsewhere. - if (process.platform !== "win32") { - return; - } - // The bug needs the temp dir and cwd on different drive roots; skip the body on single-drive machines. - if (parse(tmpdir()).root.toLowerCase() === parse(process.cwd()).root.toLowerCase()) { - return; - } // given - const root = mkdtempSync(join(tmpdir(), "senpi-rules-project-root-")); - const startPath = join(root, "plain", "nested"); - mkdirSync(startPath, { recursive: true }); + expect(win32.dirname("C:\\")).toBe("C:\\"); - try { - // when - const result = findProjectRoot(startPath, [".nonexistent-marker-senpi"]); + // when + const result = findProjectRoot(startPath, [".nonexistent-marker-senpi"]); - // then - expect(result).toBeNull(); - } finally { - rmSync(root, { recursive: true, force: true }); - } + // then + expect(result).toBeNull(); }); });