diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e3cfd3..d3cb057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/patching.md b/docs/patching.md index bf5c849..706c1cf 100644 --- a/docs/patching.md +++ b/docs/patching.md @@ -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 diff --git a/src/fix.ts b/src/fix.ts index 316b501..340ff9a 100644 --- a/src/fix.ts +++ b/src/fix.ts @@ -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, diff --git a/src/workflow.test.ts b/src/workflow.test.ts index 3dec4bc..98e0c4b 100644 --- a/src/workflow.test.ts +++ b/src/workflow.test.ts @@ -1,3 +1,4 @@ +import { providerByName } from "./provider.js"; import { describe, expect, it, vi } from "vitest"; import { access, @@ -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"], + }); + }); });