From 04fd8f47022163583df3fc1b73e4c803f1d8759d Mon Sep 17 00:00:00 2001 From: Todd Dailey Date: Fri, 15 May 2026 22:25:50 -0700 Subject: [PATCH 1/2] Add revalidate progress output --- src/app.ts | 46 +++++++++++++++++++++++++++++++++++++++++++- src/workflow.test.ts | 12 +++++++++++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/app.ts b/src/app.ts index 828304b..5f957c3 100644 --- a/src/app.ts +++ b/src/app.ts @@ -476,8 +476,19 @@ export async function revalidateCommand( await writeRun(loaded.paths, run); const results: Array<{ finding: string; outcome: FindingRecord["status"]; reasoning: string }> = []; + emitRevalidateProgress(context, "start", { + run: currentRunId, + findings: findings.length, + }); try { - for (const finding of findings) { + for (const [index, finding] of findings.entries()) { + const started = Date.now(); + emitRevalidateProgress(context, "finding-start", { + index: index + 1, + total: findings.length, + finding: finding.findingId, + title: finding.title, + }); const prompt = await buildRevalidatePrompt(loaded.root, JSON.stringify(finding, null, 2)); const output = await provider.revalidate(loaded.root, prompt, config.provider.model); const updated = appendFindingHistory( @@ -503,6 +514,13 @@ export async function revalidateCommand( outcome: output.outcome, reasoning: output.reasoning, }); + emitRevalidateProgress(context, "finding-done", { + index: index + 1, + total: findings.length, + finding: finding.findingId, + outcome: output.outcome, + elapsed: `${Math.round((Date.now() - started) / 1000)}s`, + }); } await writeRun(loaded.paths, { ...run, @@ -510,6 +528,14 @@ export async function revalidateCommand( finishedAt: nowIso(), findingIds: results.map((result) => result.finding), }); + emitRevalidateProgress(context, "done", { + run: currentRunId, + revalidated: results.length, + fixed: results.filter((result) => result.outcome === "fixed").length, + open: results.filter((result) => result.outcome === "open").length, + uncertain: results.filter((result) => result.outcome === "uncertain").length, + falsePositive: results.filter((result) => result.outcome === "false-positive").length, + }); } catch (error: unknown) { const message = error instanceof Error ? error.message : String(error); await writeRun(loaded.paths, { @@ -519,6 +545,10 @@ export async function revalidateCommand( findingIds: run.findingIds, errors: [{ message, code: error instanceof ClawpatchError ? error.code : null }], }); + emitRevalidateProgress(context, "failed", { + run: currentRunId, + error: message, + }); throw error; } if (flags["all"] === true) { @@ -905,6 +935,20 @@ function emitReviewProgress( process.stderr.write(`clawpatch review ${event}${values.length > 0 ? ` ${values}` : ""}\n`); } +function emitRevalidateProgress( + context: AppContext, + event: string, + fields: Record, +): void { + if (context.options.quiet) { + return; + } + const values = Object.entries(fields) + .map(([key, value]) => `${key}=${String(value)}`) + .join(" "); + process.stderr.write(`clawpatch revalidate ${event}${values.length > 0 ? ` ${values}` : ""}\n`); +} + function lockFeature(feature: FeatureRecord, currentRunId: string): FeatureRecord { if (feature.lock !== null) { throw new ClawpatchError(`feature locked: ${feature.featureId}`, 7, "lock-conflict"); diff --git a/src/workflow.test.ts b/src/workflow.test.ts index db8903b..489d982 100644 --- a/src/workflow.test.ts +++ b/src/workflow.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { access, mkdir, readFile, rm, symlink, unlink } from "node:fs/promises"; import { join } from "node:path"; import { @@ -275,7 +275,13 @@ describe("workflow", () => { await writeFinding(paths, { ...finding, reasoning: markers[index] ?? "" }); } + let progress = ""; + const stderr = vi.spyOn(process.stderr, "write").mockImplementation((chunk) => { + progress += String(chunk); + return true; + }); const result = await revalidateCommand(context, { all: true, status: "open", limit: "4" }); + stderr.mockRestore(); const updated = await readFindings(paths); const features = await readFeatures(paths); @@ -293,6 +299,10 @@ describe("workflow", () => { "uncertain", ]); expect(updated.every((finding) => finding.history.at(-1)?.kind === "revalidate")).toBe(true); + expect(progress).toContain("clawpatch revalidate start"); + expect(progress).toContain("clawpatch revalidate finding-start"); + expect(progress).toContain("clawpatch revalidate finding-done"); + expect(progress).toContain("clawpatch revalidate done"); const uncertain = updated.find((finding) => finding.status === "uncertain"); const uncertainFeature = features.find((feature) => feature.featureId === uncertain?.featureId); expect(uncertainFeature?.status).toBe("needs-fix"); From 511a3a270e94b312c20117aa8141213fb817749e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 16 May 2026 13:26:52 +0100 Subject: [PATCH 2/2] docs: credit revalidate progress --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 980e7e9..6c6c86a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Added Next.js route mapping for `src/app` and `src/pages` layouts, thanks @obatried. - Added first-pass Python mapping for project metadata, console scripts, source groups, pytest suites, and conservative validation defaults, thanks @xiamx. +- Added progress output for `clawpatch revalidate`, thanks @twidtwid. - Improved Node/TypeScript mapping for large workspaces by splitting package source trees into bounded review groups with package-local tests. - Added generic nested SwiftPM, Apple/Xcode, and Gradle/Android app mapping.