diff --git a/docs/writeback-evolution.md b/docs/writeback-evolution.md index 42f1e29..28e84a5 100644 --- a/docs/writeback-evolution.md +++ b/docs/writeback-evolution.md @@ -360,6 +360,12 @@ fclt ai evolve verify EV-00001 \ --note "The producing loop no longer repeats the failure" ``` +`evolve show --json` inspects a proposal without changing it. Repeating +`evolve draft ` preserves an existing draft, patch, and review state; use +`--append` only for an intentional substantive revision. If a patch is missing, +drafting rebuilds it from the existing authored draft rather than replacing that +draft with generated boilerplate. + Applying a proposal moves its source writebacks into an awaiting-verification state. Verification then records one of `improved`, `unchanged`, `regressed`, or `inconclusive`. Improved evidence resolves the writebacks; unchanged or regressed evidence returns them to the pending queue; diff --git a/src/ai.test.ts b/src/ai.test.ts index 52ab829..292e67b 100644 --- a/src/ai.test.ts +++ b/src/ai.test.ts @@ -1367,12 +1367,48 @@ describe("ai writeback", () => { "Require post-apply effectiveness verification." ); + const beforeRepeat = await showProposal(proposal!.id, { + homeDir: tempHome, + rootDir, + }); + const authoredDraftPath = join( + facultAiDraftDir(tempHome, rootDir), + `${proposal!.id}.md` + ); + const authoredPatchPath = join( + facultAiDraftDir(tempHome, rootDir), + `${proposal!.id}.patch` + ); + const authoredDraft = await readFile(authoredDraftPath, "utf8"); + const journalBefore = await readFile( + facultAiJournalPath(tempHome, rootDir), + "utf8" + ); + expect( + await draftProposal(proposal!.id, { homeDir: tempHome, rootDir }) + ).toEqual(beforeRepeat!); + expect(await readFile(authoredDraftPath, "utf8")).toBe(authoredDraft); + expect(await readFile(authoredPatchPath, "utf8")).toBe(revisedPatch); + expect(await readFile(facultAiJournalPath(tempHome, rootDir), "utf8")).toBe( + journalBefore + ); + + await rm(authoredPatchPath); + await draftProposal(proposal!.id, { homeDir: tempHome, rootDir }); + expect((await readFile(authoredDraftPath, "utf8")).trimEnd()).toBe( + authoredDraft.trimEnd() + ); + expect(await readFile(authoredPatchPath, "utf8")).toBe(revisedPatch); + const accepted = await acceptProposal(proposal!.id, { homeDir: tempHome, rootDir, }); expect(accepted.status).toBe("accepted"); expect(accepted.review?.status).toBe("accepted"); + expect( + await draftProposal(proposal!.id, { homeDir: tempHome, rootDir }) + ).toEqual(accepted); const applied = await applyProposal(proposal!.id, { homeDir: tempHome, diff --git a/src/ai.ts b/src/ai.ts index cded875..6185c85 100644 --- a/src/ai.ts +++ b/src/ai.ts @@ -2205,6 +2205,20 @@ export async function draftProposal( if (!current) { throw new Error(`Proposal not found: ${id}`); } + const draftPath = draftRefForProposal(homeDir, args.rootDir, id); + const patchPath = patchRefForProposal(homeDir, args.rootDir, id); + const existingDraftPath = await firstExistingFile([ + draftPath, + ...current.draftRefs.filter((pathValue) => pathValue.endsWith(".md")), + ]); + if ( + current.status !== "proposed" && + !args.append && + existingDraftPath && + (await fileExists(patchPath)) + ) { + return current; + } const writebacks = ( await Promise.all( current.sourceWritebacks.map(async (writebackId) => { @@ -2220,24 +2234,17 @@ export async function draftProposal( homeDir, rootDir: args.rootDir, }); - const draftPath = draftRefForProposal(homeDir, args.rootDir, id); - const patchPath = patchRefForProposal(homeDir, args.rootDir, id); await mkdir(dirname(draftPath), { recursive: true }); const generatedBody = renderDraftBody(current, writebacks); - const existingDraftPath = await firstExistingFile([ - draftPath, - ...current.draftRefs.filter((pathValue) => pathValue.endsWith(".md")), - ]); - const priorDraft = - args.append && existingDraftPath - ? await readFile(existingDraftPath, "utf8") - : null; + const priorDraft = existingDraftPath + ? await readFile(existingDraftPath, "utf8") + : null; const baseDraft = priorDraft ?? generatedBody; const draftBody = args.append ? isAppendProposalKind(current.kind) ? insertDraftRevision(id, baseDraft, args.append) : `${baseDraft.trimEnd()}\n\n## Draft Revision\n${args.append.trim()}\n` - : generatedBody; + : baseDraft; await Bun.write(draftPath, `${draftBody}\n`); const currentText = (await fileExists(targetNode.path!)) ? await readFile(targetNode.path!, "utf8")