Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/commands/batchApply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
34 changes: 34 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
40 changes: 40 additions & 0 deletions test/unit/initializeProject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
Expand Down
Loading