@tritard/waterbrother 0.8.14 → 0.8.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.js +37 -8
- package/src/frontend.js +146 -1
- package/src/workflow.js +34 -4
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -673,6 +673,13 @@ function createProgressSpinner(initialLabel = "thinking...") {
|
|
|
673
673
|
let label = initialLabel;
|
|
674
674
|
let frameIndex = 0;
|
|
675
675
|
let stopped = false;
|
|
676
|
+
const renderLabel = () => {
|
|
677
|
+
if (typeof label === "function") {
|
|
678
|
+
const next = label();
|
|
679
|
+
return String(next || "");
|
|
680
|
+
}
|
|
681
|
+
return String(label || "");
|
|
682
|
+
};
|
|
676
683
|
const clearLine = () => {
|
|
677
684
|
process.stdout.write("\r\x1b[2K");
|
|
678
685
|
};
|
|
@@ -686,7 +693,7 @@ function createProgressSpinner(initialLabel = "thinking...") {
|
|
|
686
693
|
const bar = renderFlowingBar(frameIndex, 14);
|
|
687
694
|
frameIndex += 1;
|
|
688
695
|
clearLine();
|
|
689
|
-
process.stdout.write(`${styleAssistantPrefix()} ${bar} ${
|
|
696
|
+
process.stdout.write(`${styleAssistantPrefix()} ${bar} ${renderLabel()}`);
|
|
690
697
|
}, 70);
|
|
691
698
|
|
|
692
699
|
let activeInterval = interval;
|
|
@@ -714,7 +721,7 @@ function createProgressSpinner(initialLabel = "thinking...") {
|
|
|
714
721
|
const bar = renderFlowingBar(frameIndex, 14);
|
|
715
722
|
frameIndex += 1;
|
|
716
723
|
clearLine();
|
|
717
|
-
process.stdout.write(`${styleAssistantPrefix()} ${bar} ${
|
|
724
|
+
process.stdout.write(`${styleAssistantPrefix()} ${bar} ${renderLabel()}`);
|
|
718
725
|
}, 70);
|
|
719
726
|
activeSpinnerController = controller;
|
|
720
727
|
}
|
|
@@ -724,6 +731,17 @@ function createProgressSpinner(initialLabel = "thinking...") {
|
|
|
724
731
|
return controller;
|
|
725
732
|
}
|
|
726
733
|
|
|
734
|
+
function formatElapsedShort(ms) {
|
|
735
|
+
const totalSeconds = Math.max(0, Math.floor(Number(ms || 0) / 1000));
|
|
736
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
737
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
738
|
+
const seconds = totalSeconds % 60;
|
|
739
|
+
if (minutes < 60) return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
740
|
+
const hours = Math.floor(minutes / 60);
|
|
741
|
+
const remMinutes = minutes % 60;
|
|
742
|
+
return remMinutes > 0 ? `${hours}h ${remMinutes}m` : `${hours}h`;
|
|
743
|
+
}
|
|
744
|
+
|
|
727
745
|
function parseToolResultShape(resultText) {
|
|
728
746
|
try {
|
|
729
747
|
const parsed = JSON.parse(String(resultText || ""));
|
|
@@ -864,6 +882,15 @@ function printReceiptSummary(receipt) {
|
|
|
864
882
|
if (receipt.designRevision?.triggered) {
|
|
865
883
|
console.log(`${styleSystemPrefix()} ${dim("design pass")} ${yellow("auto-revised")} ${receipt.designRevision.initialSummary || ""}`.trim());
|
|
866
884
|
}
|
|
885
|
+
if (receipt.screenshotReview?.verdict) {
|
|
886
|
+
const verdict =
|
|
887
|
+
receipt.screenshotReview.verdict === "strong"
|
|
888
|
+
? green("strong")
|
|
889
|
+
: receipt.screenshotReview.verdict === "weak"
|
|
890
|
+
? red("weak")
|
|
891
|
+
: yellow("caution");
|
|
892
|
+
console.log(`${styleSystemPrefix()} ${dim("render")} ${verdict} ${receipt.screenshotReview.summary || ""}`.trim());
|
|
893
|
+
}
|
|
867
894
|
}
|
|
868
895
|
|
|
869
896
|
function printImpactMap(impact) {
|
|
@@ -3400,14 +3427,12 @@ async function runTextTurnInteractive({
|
|
|
3400
3427
|
const idleMs = Date.now() - lastProgressAt;
|
|
3401
3428
|
if (!heartbeatFired && idleMs >= 2000) {
|
|
3402
3429
|
heartbeatFired = true;
|
|
3430
|
+
spinner.setLabel(() => `• Working (${formatElapsedShort(Date.now() - turnSummary.startedAt)} • esc to interrupt)`);
|
|
3403
3431
|
printLiveTrace(`state=${currentState}...`, context.runtime.traceMode, { verboseOnly: true });
|
|
3404
3432
|
}
|
|
3405
3433
|
if (!stalledNotified && idleMs >= 8000) {
|
|
3406
3434
|
stalledNotified = true;
|
|
3407
|
-
|
|
3408
|
-
activeSpinnerController.clear();
|
|
3409
|
-
}
|
|
3410
|
-
console.log(`${styleSystemPrefix()} ${yellow(`still working (${currentState}) — press Ctrl+C to interrupt`)}`);
|
|
3435
|
+
spinner.setLabel(() => `• Working (${formatElapsedShort(Date.now() - turnSummary.startedAt)} • esc to interrupt)`);
|
|
3411
3436
|
}
|
|
3412
3437
|
}, 500);
|
|
3413
3438
|
|
|
@@ -5263,8 +5288,7 @@ async function promptLoop(agent, session, context) {
|
|
|
5263
5288
|
const idleMs = Date.now() - lastProgressAt;
|
|
5264
5289
|
if (!stalledNotified && idleMs >= 8000) {
|
|
5265
5290
|
stalledNotified = true;
|
|
5266
|
-
|
|
5267
|
-
console.log(`${styleSystemPrefix()} ${yellow(`still working (${currentState}) — press Ctrl+C to interrupt`)}`);
|
|
5291
|
+
spinner.setLabel(() => `• Working (${formatElapsedShort(Date.now() - turnSummary.startedAt)} • esc to interrupt)`);
|
|
5268
5292
|
}
|
|
5269
5293
|
}, 500);
|
|
5270
5294
|
|
|
@@ -5387,6 +5411,11 @@ async function promptLoop(agent, session, context) {
|
|
|
5387
5411
|
const vc = v === "strong" ? green(v) : v === "weak" ? red(v) : yellow(v);
|
|
5388
5412
|
lines.push(`${dim("design:")} ${vc} — ${buildResult.designReview.summary}`);
|
|
5389
5413
|
}
|
|
5414
|
+
if (buildResult.screenshotReview) {
|
|
5415
|
+
const v = buildResult.screenshotReview.verdict;
|
|
5416
|
+
const vc = v === "strong" ? green(v) : v === "weak" ? red(v) : yellow(v);
|
|
5417
|
+
lines.push(`${dim("render:")} ${vc} — ${buildResult.screenshotReview.summary}`);
|
|
5418
|
+
}
|
|
5390
5419
|
if (buildResult.designRevision?.triggered) {
|
|
5391
5420
|
lines.push(`${dim("design pass:")} ${yellow("auto-revised")} — ${buildResult.designRevision.initialSummary || "first pass revised"}`);
|
|
5392
5421
|
}
|
package/src/frontend.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { promisify } from "node:util";
|
|
1
7
|
import { createChatCompletion } from "./grok-client.js";
|
|
2
8
|
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
|
|
3
11
|
const FRONTEND_REVIEW_SYSTEM_PROMPT = `You are Waterbrother Art Director, a strict frontend design critic.
|
|
4
12
|
You review generated websites, landing pages, blogs, and UI surfaces after implementation.
|
|
5
13
|
Judge the work against these standards:
|
|
@@ -23,6 +31,22 @@ Rules:
|
|
|
23
31
|
- Call out fake credibility badges, placeholder brands, stock-image dependence, Tailwind CDN template styling, and overused serif/sans premium-blog tropes when present.
|
|
24
32
|
- Do not wrap JSON in markdown.`;
|
|
25
33
|
|
|
34
|
+
const FRONTEND_SCREENSHOT_REVIEW_SYSTEM_PROMPT = `You are Waterbrother Art Director reviewing a rendered website screenshot.
|
|
35
|
+
Judge the actual visual result on screen, not the code alone.
|
|
36
|
+
|
|
37
|
+
Return strict JSON with keys:
|
|
38
|
+
- verdict: one of strong, caution, weak
|
|
39
|
+
- summary: short string
|
|
40
|
+
- wins: array of strings
|
|
41
|
+
- visualIssues: array of strings
|
|
42
|
+
- nextPass: array of strings
|
|
43
|
+
|
|
44
|
+
Rules:
|
|
45
|
+
- Use weak for obvious template output, weak hierarchy, awkward spacing, generic composition, or visually incoherent pages.
|
|
46
|
+
- Use caution for competent pages that still feel safe, generic, or under-directed.
|
|
47
|
+
- Be concrete about visible layout, spacing, typography, contrast, composition, and interaction cues.
|
|
48
|
+
- Do not wrap JSON in markdown.`;
|
|
49
|
+
|
|
26
50
|
const SITE_TYPES = [
|
|
27
51
|
["blog", /\b(blog|journal|essays?|articles?|publication|editorial)\b/i],
|
|
28
52
|
["landing", /\b(landing page|homepage|home page|marketing site|product site|launch page|hero section)\b/i],
|
|
@@ -102,6 +126,21 @@ function normalizeContent(content) {
|
|
|
102
126
|
return "";
|
|
103
127
|
}
|
|
104
128
|
|
|
129
|
+
function inferMimeType(filePath) {
|
|
130
|
+
const lower = String(filePath || "").toLowerCase();
|
|
131
|
+
if (lower.endsWith(".png")) return "image/png";
|
|
132
|
+
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
133
|
+
if (lower.endsWith(".webp")) return "image/webp";
|
|
134
|
+
if (lower.endsWith(".gif")) return "image/gif";
|
|
135
|
+
return "application/octet-stream";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function loadImageAsDataUrl(filePath) {
|
|
139
|
+
const raw = await fs.readFile(filePath);
|
|
140
|
+
const mime = inferMimeType(filePath);
|
|
141
|
+
return `data:${mime};base64,${raw.toString("base64")}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
105
144
|
function parseJsonObject(text) {
|
|
106
145
|
const raw = String(text || "").trim();
|
|
107
146
|
if (!raw) return null;
|
|
@@ -237,17 +276,22 @@ export function shouldAutoReviseFrontend({ designReview = null, slop = null, rev
|
|
|
237
276
|
export function buildFrontendRevisionPrompt({
|
|
238
277
|
originalPrompt = "",
|
|
239
278
|
designReview = null,
|
|
240
|
-
slop = null
|
|
279
|
+
slop = null,
|
|
280
|
+
screenshotReview = null
|
|
241
281
|
} = {}) {
|
|
242
282
|
const issues = Array.isArray(designReview?.issues) ? designReview.issues.slice(0, 6) : [];
|
|
243
283
|
const nextPass = Array.isArray(designReview?.nextPass) ? designReview.nextPass.slice(0, 6) : [];
|
|
244
284
|
const slopFlags = Array.isArray(slop?.flags) ? slop.flags.slice(0, 6) : [];
|
|
285
|
+
const visualIssues = Array.isArray(screenshotReview?.visualIssues) ? screenshotReview.visualIssues.slice(0, 6) : [];
|
|
286
|
+
const visualNextPass = Array.isArray(screenshotReview?.nextPass) ? screenshotReview.nextPass.slice(0, 6) : [];
|
|
245
287
|
const blocks = [
|
|
246
288
|
`Revise the generated frontend to address the design problems from the first pass.`,
|
|
247
289
|
`Original task: ${String(originalPrompt || "").trim()}`,
|
|
248
290
|
issues.length > 0 ? `Problems to fix:\n- ${issues.join("\n- ")}` : "",
|
|
291
|
+
visualIssues.length > 0 ? `Visible screenshot problems:\n- ${visualIssues.join("\n- ")}` : "",
|
|
249
292
|
slopFlags.length > 0 ? `Deterministic slop flags:\n- ${slopFlags.join("\n- ")}` : "",
|
|
250
293
|
nextPass.length > 0 ? `Revision priorities:\n- ${nextPass.join("\n- ")}` : "",
|
|
294
|
+
visualNextPass.length > 0 ? `Screenshot revision priorities:\n- ${visualNextPass.join("\n- ")}` : "",
|
|
251
295
|
"Do not add new filler sections.",
|
|
252
296
|
"Do not add fake prestige, fake testimonials, fake brands, or placeholder-image services.",
|
|
253
297
|
"Simplify the page if needed. Stronger direction with fewer elements is preferred over busier generic output.",
|
|
@@ -267,6 +311,17 @@ function normalizeFrontendReview(review) {
|
|
|
267
311
|
};
|
|
268
312
|
}
|
|
269
313
|
|
|
314
|
+
function normalizeScreenshotReview(review) {
|
|
315
|
+
const verdict = String(review?.verdict || "caution").trim().toLowerCase();
|
|
316
|
+
return {
|
|
317
|
+
verdict: ["strong", "caution", "weak"].includes(verdict) ? verdict : "caution",
|
|
318
|
+
summary: String(review?.summary || "No screenshot review summary returned.").trim(),
|
|
319
|
+
wins: Array.isArray(review?.wins) ? review.wins.map((item) => String(item || "").trim()).filter(Boolean).slice(0, 8) : [],
|
|
320
|
+
visualIssues: Array.isArray(review?.visualIssues) ? review.visualIssues.map((item) => String(item || "").trim()).filter(Boolean).slice(0, 8) : [],
|
|
321
|
+
nextPass: Array.isArray(review?.nextPass) ? review.nextPass.map((item) => String(item || "").trim()).filter(Boolean).slice(0, 8) : []
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
270
325
|
export async function reviewFrontendTurn({
|
|
271
326
|
apiKey,
|
|
272
327
|
baseUrl,
|
|
@@ -300,3 +355,93 @@ export async function reviewFrontendTurn({
|
|
|
300
355
|
const parsed = parseJsonObject(normalizeContent(completion?.message?.content));
|
|
301
356
|
return normalizeFrontendReview(parsed || {});
|
|
302
357
|
}
|
|
358
|
+
|
|
359
|
+
export async function findFrontendPreviewEntry({ cwd, receipt = null } = {}) {
|
|
360
|
+
const changedFiles = Array.isArray(receipt?.changedFiles) ? receipt.changedFiles : [];
|
|
361
|
+
for (const filePath of changedFiles) {
|
|
362
|
+
const raw = String(filePath || "").trim();
|
|
363
|
+
if (!raw) continue;
|
|
364
|
+
const absolute = path.isAbsolute(raw) ? raw : path.resolve(cwd || process.cwd(), raw);
|
|
365
|
+
if (!absolute.toLowerCase().endsWith(".html")) continue;
|
|
366
|
+
try {
|
|
367
|
+
await fs.access(absolute);
|
|
368
|
+
return absolute;
|
|
369
|
+
} catch {}
|
|
370
|
+
}
|
|
371
|
+
const fallback = path.resolve(cwd || process.cwd(), "index.html");
|
|
372
|
+
try {
|
|
373
|
+
await fs.access(fallback);
|
|
374
|
+
return fallback;
|
|
375
|
+
} catch {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export async function captureFrontendScreenshot({ entryPath } = {}) {
|
|
381
|
+
if (!entryPath || process.platform !== "darwin") return null;
|
|
382
|
+
const screenshotPath = path.join(os.tmpdir(), `waterbrother-frontend-${Date.now()}.png`);
|
|
383
|
+
const targetUrl = pathToFileURL(entryPath).toString();
|
|
384
|
+
const script = `
|
|
385
|
+
on run argv
|
|
386
|
+
set targetUrl to item 1 of argv
|
|
387
|
+
set outputPath to item 2 of argv
|
|
388
|
+
set leftPos to 72
|
|
389
|
+
set topPos to 56
|
|
390
|
+
set rightPos to 1512
|
|
391
|
+
set bottomPos to 1120
|
|
392
|
+
tell application "Safari"
|
|
393
|
+
activate
|
|
394
|
+
make new document with properties {URL:targetUrl}
|
|
395
|
+
delay 2
|
|
396
|
+
try
|
|
397
|
+
set bounds of front window to {leftPos, topPos, rightPos, bottomPos}
|
|
398
|
+
end try
|
|
399
|
+
delay 1
|
|
400
|
+
set winBounds to bounds of front window
|
|
401
|
+
end tell
|
|
402
|
+
set captureLeft to item 1 of winBounds
|
|
403
|
+
set captureTop to item 2 of winBounds
|
|
404
|
+
set captureWidth to (item 3 of winBounds) - (item 1 of winBounds)
|
|
405
|
+
set captureHeight to (item 4 of winBounds) - (item 2 of winBounds)
|
|
406
|
+
do shell script "/usr/sbin/screencapture -x -R" & quoted form of ((captureLeft as string) & "," & (captureTop as string) & "," & (captureWidth as string) & "," & (captureHeight as string)) & " " & quoted form of outputPath
|
|
407
|
+
tell application "Safari"
|
|
408
|
+
try
|
|
409
|
+
close front document saving no
|
|
410
|
+
end try
|
|
411
|
+
end tell
|
|
412
|
+
return outputPath
|
|
413
|
+
end run`;
|
|
414
|
+
await execFileAsync("/usr/bin/osascript", ["-e", script, targetUrl, screenshotPath], { timeout: 12000 });
|
|
415
|
+
return screenshotPath;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export async function reviewFrontendScreenshot({
|
|
419
|
+
apiKey,
|
|
420
|
+
baseUrl,
|
|
421
|
+
model,
|
|
422
|
+
screenshotPath,
|
|
423
|
+
promptText = "",
|
|
424
|
+
signal
|
|
425
|
+
} = {}) {
|
|
426
|
+
if (!screenshotPath) return null;
|
|
427
|
+
const imageUrl = await loadImageAsDataUrl(screenshotPath);
|
|
428
|
+
const completion = await createChatCompletion({
|
|
429
|
+
apiKey,
|
|
430
|
+
baseUrl,
|
|
431
|
+
model,
|
|
432
|
+
signal,
|
|
433
|
+
messages: [
|
|
434
|
+
{ role: "system", content: FRONTEND_SCREENSHOT_REVIEW_SYSTEM_PROMPT },
|
|
435
|
+
{
|
|
436
|
+
role: "user",
|
|
437
|
+
content: [
|
|
438
|
+
{ type: "text", text: `Audit this rendered frontend screenshot for the task: ${String(promptText || "").slice(0, 4000)}` },
|
|
439
|
+
{ type: "image_url", image_url: { url: imageUrl } }
|
|
440
|
+
]
|
|
441
|
+
}
|
|
442
|
+
]
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const parsed = parseJsonObject(normalizeContent(completion?.message?.content));
|
|
446
|
+
return normalizeScreenshotReview(parsed || {});
|
|
447
|
+
}
|
package/src/workflow.js
CHANGED
|
@@ -4,8 +4,11 @@ import { reviewTurn, challengeReceipt } from "./reviewer.js";
|
|
|
4
4
|
import {
|
|
5
5
|
buildFrontendExecutionContext,
|
|
6
6
|
buildFrontendRevisionPrompt,
|
|
7
|
+
captureFrontendScreenshot,
|
|
7
8
|
detectFrontendSlop,
|
|
9
|
+
findFrontendPreviewEntry,
|
|
8
10
|
reviewFrontendTurn,
|
|
11
|
+
reviewFrontendScreenshot,
|
|
9
12
|
shouldAutoReviseFrontend,
|
|
10
13
|
shouldRunFrontendReview
|
|
11
14
|
} from "./frontend.js";
|
|
@@ -152,14 +155,37 @@ export async function runBuildWorkflow({
|
|
|
152
155
|
}
|
|
153
156
|
}
|
|
154
157
|
|
|
158
|
+
let screenshotReview = null;
|
|
159
|
+
let screenshotPath = null;
|
|
160
|
+
if (designReview) {
|
|
161
|
+
try {
|
|
162
|
+
const previewEntry = await findFrontendPreviewEntry({ cwd: context.cwd, receipt: activeReceipt });
|
|
163
|
+
if (previewEntry) {
|
|
164
|
+
screenshotPath = await captureFrontendScreenshot({ entryPath: previewEntry });
|
|
165
|
+
if (screenshotPath) {
|
|
166
|
+
screenshotReview = await reviewFrontendScreenshot({
|
|
167
|
+
apiKey: context.runtime.apiKey,
|
|
168
|
+
baseUrl: context.runtime.baseUrl,
|
|
169
|
+
model: context.runtime.reviewer?.model || agent.getModel(),
|
|
170
|
+
screenshotPath,
|
|
171
|
+
promptText,
|
|
172
|
+
signal: handlers.signal
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
screenshotReview = null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
155
181
|
const designSlop = designReview
|
|
156
182
|
? detectFrontendSlop({ promptText, assistantText: activeResponse.content || "", receipt: activeReceipt, designReview })
|
|
157
183
|
: null;
|
|
158
184
|
|
|
159
|
-
return { impact, review, designReview, designSlop };
|
|
185
|
+
return { impact, review, designReview, designSlop, screenshotReview, screenshotPath };
|
|
160
186
|
}
|
|
161
187
|
|
|
162
|
-
let { impact, review, designReview, designSlop } = await analyze(receipt, response);
|
|
188
|
+
let { impact, review, designReview, designSlop, screenshotReview, screenshotPath } = await analyze(receipt, response);
|
|
163
189
|
let designRevision = null;
|
|
164
190
|
|
|
165
191
|
if (shouldAutoReviseFrontend({ designReview, slop: designSlop, revisionCount: 0 })) {
|
|
@@ -169,7 +195,8 @@ export async function runBuildWorkflow({
|
|
|
169
195
|
const revisionPrompt = buildFrontendRevisionPrompt({
|
|
170
196
|
originalPrompt: promptText,
|
|
171
197
|
designReview,
|
|
172
|
-
slop: designSlop
|
|
198
|
+
slop: designSlop,
|
|
199
|
+
screenshotReview
|
|
173
200
|
});
|
|
174
201
|
const revisionCtx = {
|
|
175
202
|
...executionCtx,
|
|
@@ -187,7 +214,7 @@ export async function runBuildWorkflow({
|
|
|
187
214
|
const revisedReceipt = await agent.toolRuntime.completeTurn({ signal: handlers.signal });
|
|
188
215
|
if (revisedReceipt) {
|
|
189
216
|
receipt = revisedReceipt;
|
|
190
|
-
({ impact, review, designReview, designSlop } = await analyze(receipt, response));
|
|
217
|
+
({ impact, review, designReview, designSlop, screenshotReview, screenshotPath } = await analyze(receipt, response));
|
|
191
218
|
designRevision = {
|
|
192
219
|
triggered: true,
|
|
193
220
|
firstPassVerdict,
|
|
@@ -203,6 +230,8 @@ export async function runBuildWorkflow({
|
|
|
203
230
|
if (review) updates.review = review;
|
|
204
231
|
if (designReview) updates.designReview = designReview;
|
|
205
232
|
if (designSlop) updates.designSlop = designSlop;
|
|
233
|
+
if (screenshotReview) updates.screenshotReview = screenshotReview;
|
|
234
|
+
if (screenshotPath) updates.screenshotPath = screenshotPath;
|
|
206
235
|
if (designRevision) updates.designRevision = designRevision;
|
|
207
236
|
let finalReceipt = receipt;
|
|
208
237
|
if (Object.keys(updates).length > 0) {
|
|
@@ -229,6 +258,7 @@ export async function runBuildWorkflow({
|
|
|
229
258
|
impact,
|
|
230
259
|
review,
|
|
231
260
|
designReview,
|
|
261
|
+
screenshotReview,
|
|
232
262
|
impactSummary: impact ? summarizeImpactMap(impact) : null
|
|
233
263
|
};
|
|
234
264
|
}
|