diff --git a/AGENTS.md b/AGENTS.md index 450e04e..c80a677 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,7 +50,7 @@ test/ batchApply.test.ts Batch template and operation count parsing (15 tests) binary.test.ts Binary discovery, managed install, compatibility, workspace env (59 tests) binaryDiscovery.test.ts Real executable discovery on PATH (13 tests) - initializeProject.test.ts Status display, agents file classification, formatError (26 tests) + initializeProject.test.ts Status display, agents file classification, formatError (29 tests) managedLifecycle.test.ts Managed install with real file I/O (22 tests) mcpConfig.test.ts MCP config with real temp directories (9 tests) outputChannel.test.ts Output channel logging wrapper (10 tests) diff --git a/README.md b/README.md index 2feada7..fc2e067 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,13 @@ The extension detects outdated CLI builds and warns with upgrade guidance. It re Set `patchloom.path` in settings, or add the CLI to your `PATH`. **CLI compatibility warning** -Run `Patchloom: Open Releases` to download the latest release. The extension requires 0.3.0 or newer; 0.16.0 is recommended. +Run `Patchloom: Open Releases` to download the latest release. The extension requires 0.3.0 or newer; 0.18.0 is recommended. + +**Path rejected by workspace guard** +Quick Actions and Batch Apply pass `--contain` so paths stay inside the open workspace folder. On CLI 0.18+, sandbox escapes report `error_kind: guard_rejected` (not a generic `invalid_input`). Keep targets under the workspace root, or open the folder that owns the files. + +**Batch replace shape** +Batch lines use `replace PATH OLD NEW` (and optional flags such as `--fuzzy`). Do not paste CLI form `replace OLD --new NEW path` into a batch plan; CLI 0.18+ returns a clear parse error with the PATH OLD NEW hint. **MCP config not injected** Run `Patchloom: Configure MCP` and select the target editor config. @@ -205,7 +211,7 @@ File bugs and feature requests at [patchloom/patchloom-vscode/issues](https://gi ## Requirements - VS Code 1.90 or newer (or compatible editors: Cursor, Windsurf, VSCodium) -- [Patchloom CLI](https://github.com/patchloom/patchloom) 0.3.0 or newer (0.16.0+ recommended for multi-doc `doc merge --selector`, line-oriented `insert_before`/`insert_after`, shared binary/UTF-8 path honesty, 56 MCP tools, JSON `applied` honesty, doc query envelopes, `md insert-after-section`, optional `--contain` path guarding, and fuzzy replace floors) +- [Patchloom CLI](https://github.com/patchloom/patchloom) 0.3.0 or newer (0.18.0+ recommended for structured `guard_rejected` on `--contain` failures, clearer batch `replace PATH OLD NEW` parse hints, multi-doc `doc merge --selector`, line-oriented `insert_before`/`insert_after`, 56 MCP tools, JSON `applied` honesty, and agent-facing `error_kind` envelopes) ## Contributing diff --git a/src/commands/batchApply.ts b/src/commands/batchApply.ts index 3bc8e1f..4665208 100644 --- a/src/commands/batchApply.ts +++ b/src/commands/batchApply.ts @@ -5,6 +5,7 @@ import { formatCliOutput } from "../util.js"; import { getPatchloomLog } from "../logging/outputChannel.js"; import { activeWorkspaceFolder } from "../workspace/readiness.js"; +// Batch replace is PATH OLD NEW (not CLI `replace OLD --new NEW path`). See CLI 0.18+ batch --help. export const BATCH_TEMPLATE = [ "replace src/example.ts \"old text\" \"new text\"", "replace src/example.ts \"typo_here\" \"fixed\" --fuzzy --min-fuzzy-score 0.80", diff --git a/src/util.ts b/src/util.ts index 81fc8b1..8aa0c0c 100644 --- a/src/util.ts +++ b/src/util.ts @@ -9,11 +9,45 @@ export function formatError(error: unknown): string { } } +/** + * Prefer machine-readable CLI JSON error envelopes (error_kind + error) when + * present so agents and the UI surface kinds like guard_rejected (CLI 0.18+) + * instead of a flattened multi-line dump. + */ export function formatCliOutput(result: { exitCode: number; stdout: string; stderr: string }): string { + const jsonError = extractCliJsonError(result.stdout) ?? extractCliJsonError(result.stderr); + if (jsonError !== undefined) { + return jsonError; + } + const output = `${result.stderr}\n${result.stdout}` .split(/\r?\n/) .map((line) => line.trim()) .filter((line) => line.length > 0) .join(" "); return output || `exit code ${result.exitCode}`; +} + +function extractCliJsonError(stream: string): string | undefined { + const trimmed = stream.trim(); + if (!trimmed.startsWith("{")) { + return undefined; + } + try { + const parsed = JSON.parse(trimmed) as { error?: unknown; error_kind?: unknown }; + if (typeof parsed.error !== "string" || parsed.error.length === 0) { + return undefined; + } + // CLI often prefixes "guard_rejected: …" already; avoid "kind: kind: …". + if ( + typeof parsed.error_kind === "string" && + parsed.error_kind.length > 0 && + !parsed.error.startsWith(`${parsed.error_kind}:`) + ) { + return `${parsed.error_kind}: ${parsed.error}`; + } + return parsed.error; + } catch { + return undefined; + } } \ No newline at end of file diff --git a/test/unit/initializeProject.test.ts b/test/unit/initializeProject.test.ts index 7b407d2..74a5814 100644 --- a/test/unit/initializeProject.test.ts +++ b/test/unit/initializeProject.test.ts @@ -45,6 +45,46 @@ test("formatCliOutput normalizes CRLF line endings", () => { assert.equal(result, "line1 line2"); }); +test("formatCliOutput prefers CLI JSON error envelope (guard_rejected, CLI 0.18+)", () => { + const stdout = JSON.stringify({ + ok: false, + error: "guard_rejected: path rejected by workspace guard: path escapes workspace directory: ../x", + error_kind: "guard_rejected", + applied: false + }); + const result = formatCliOutput({ exitCode: 1, stdout, stderr: "" }); + assert.equal( + result, + "guard_rejected: path rejected by workspace guard: path escapes workspace directory: ../x" + ); +}); + +test("formatCliOutput prefixes error_kind when error body omits it", () => { + const stdout = JSON.stringify({ + ok: false, + error: "path escapes workspace directory: ../x", + error_kind: "guard_rejected", + applied: false + }); + assert.equal( + formatCliOutput({ exitCode: 1, stdout, stderr: "" }), + "guard_rejected: path escapes workspace directory: ../x" + ); +}); + +test("formatCliOutput prefers JSON error over noisy stderr", () => { + const stdout = JSON.stringify({ + ok: false, + error: "line 1: batch replace does not accept CLI flag '--new'", + error_kind: "parse_error", + applied: false + }); + assert.equal( + formatCliOutput({ exitCode: 1, stdout, stderr: "some diagnostic noise\n" }), + "parse_error: line 1: batch replace does not accept CLI flag '--new'" + ); +}); + test("classifyAgentsFile returns missing when AGENTS.md does not exist", () => { assert.equal(classifyAgentsFile(undefined, "# Rules\n"), "missing"); });