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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## 0.8.1 - Unreleased

- Preserved observed source edits in failed patch attempts when a provider writes files before exiting with an error.

- Fixed nested-project repairs to ignore their own state and sibling changes, fingerprint project-relative source paths, and record both sides of renames.

- Updated Zod and development tooling, aligned Node typings with the Node 22 floor, and added Node 22/24 runtime CI with pinned GitHub Actions.
Expand Down
1 change: 1 addition & 0 deletions docs/patching.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Current behavior:
- lets the provider edit the worktree during the explicit fix command
- runs the configured formatter, feature-specific tests, typecheck, lint, and
configured test command, with duplicates removed (see [Validation](validation.md))
- records source edits even when the provider fails before validation
- records command results
- links the patch attempt to the finding

Expand Down
2 changes: 2 additions & 0 deletions src/fix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,11 @@ export async function fixCommand(
plan = await provider.fix(loaded.root, prompt, providerOptions(config));
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
const afterChanged = await sourceChangedSnapshots(loaded.root, loaded.paths.stateDir);
await writePatchAttempt(loaded.paths, {
...initialPatch,
status: "failed",
filesChanged: changedPathsBetweenSnapshots(beforeChanged, afterChanged),
plan: `${initialPatch.plan}\n\nProvider failed: ${message}`,
provider: {
name: provider.name,
Expand Down
26 changes: 26 additions & 0 deletions src/workflow.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { providerByName } from "./provider.js";
import { describe, expect, it, vi } from "vitest";
import {
access,
Expand Down Expand Up @@ -5195,4 +5196,29 @@ describe("workflow", () => {
expect(findings[0]?.linkedPatchAttemptIds).toContain(patches[0]?.patchAttemptId);
delete process.env["CLAWPATCH_PROVIDER"];
});

it("records partial edits when a provider fails after writing source", async () => {
const root = await sinceFixture("clawpatch-partial-fix-");
const context = await makeContext({ ...testOptions(root), quiet: true });
await initCommand(context, {});
await mapCommand(context);
const reviewed = (await reviewCommand(context, { provider: "mock", limit: "1" })) as {
next: string;
};
const finding = reviewed.next.split(" ").at(-1)!;
vi.spyOn(providerByName("mock-fail"), "fix").mockImplementationOnce(async () => {
await writeFixture(root, "src/one.ts", "export const one = 42;\n");
await writeFixture(root, "src/partial.ts", "export const partial = true;\n");
throw new Error("failed after editing");
});
await expect(fixCommand(context, { finding, provider: "mock-fail" })).rejects.toThrow(
"failed after editing",
);
const patches = await readPatchAttempts(statePaths(join(root, ".clawpatch")));
expect(patches).toHaveLength(1);
expect(patches[0]).toMatchObject({
status: "failed",
filesChanged: ["src/one.ts", "src/partial.ts"],
});
});
});