diff --git a/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/ListDirTool.kt b/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/ListDirTool.kt index 649d490e..8811e162 100644 --- a/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/ListDirTool.kt +++ b/infrastructure/tools/filesystem/src/main/kotlin/com/correx/infrastructure/tools/filesystem/ListDirTool.kt @@ -111,7 +111,15 @@ class ListDirTool( ToolResult.Failure(request.invocationId, msg, recoverable = true) } !Files.isDirectory(root) -> - ToolResult.Failure(request.invocationId, "Not a directory: $pathString", recoverable = true) + // Explicit file-vs-dir mismatch: a bare "Not a directory" left models re-issuing the + // identical list_dir until cancel (session a60c54b0). Name the remedy so the next + // action is unambiguous — file_read it, or delete/convert it first. + ToolResult.Failure( + request.invocationId, + "Not a directory: $pathString is a FILE, not a directory. " + + "Use file_read to read it, or delete/convert it before listing it as a directory.", + recoverable = true, + ) else -> runCatching { walk(root, recursive, request) }.getOrElse { ToolResult.Failure(request.invocationId, "Failed to list dir: ${it.message}", recoverable = false) } diff --git a/infrastructure/tools/filesystem/src/test/kotlin/com/correx/infrastructure/tools/filesystem/ListDirToolTest.kt b/infrastructure/tools/filesystem/src/test/kotlin/com/correx/infrastructure/tools/filesystem/ListDirToolTest.kt index 66a986db..6b0cda12 100644 --- a/infrastructure/tools/filesystem/src/test/kotlin/com/correx/infrastructure/tools/filesystem/ListDirToolTest.kt +++ b/infrastructure/tools/filesystem/src/test/kotlin/com/correx/infrastructure/tools/filesystem/ListDirToolTest.kt @@ -58,6 +58,19 @@ class ListDirToolTest { assertFalse(out.contains("deep"), out) } + @Test + fun `listing a file gives a recoverable file-vs-dir hint, not a bare error`(): Unit = runBlocking { + // Regression (session a60c54b0): list_dir on a plain file returned a bare "Not a directory" + // and models re-issued the identical call until cancel. The failure must name the remedy. + val root = Files.createTempDirectory("listdir") + Files.writeString(root.resolve("pages"), "i am a file") + val tool = ListDirTool(allowedPaths = setOf(root), workingDir = root) + val result = tool.execute(request(path = "pages")) as ToolResult.Failure + assertTrue(result.recoverable, "file-vs-dir mismatch is recoverable") + assertTrue(result.reason.contains("FILE"), result.reason) + assertTrue(result.reason.contains("file_read"), result.reason) + } + @Test fun `non-recursive lists only immediate children`(): Unit = runBlocking { val root = Files.createTempDirectory("listdir")