@tritard/waterbrother 0.8.13 → 0.8.14
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 +6 -0
- package/src/frontend.js +101 -1
- package/src/workflow.js +107 -54
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -861,6 +861,9 @@ function printReceiptSummary(receipt) {
|
|
|
861
861
|
: yellow("caution");
|
|
862
862
|
console.log(`${styleSystemPrefix()} ${dim("design")} ${verdict} ${receipt.designReview.summary || ""}`.trim());
|
|
863
863
|
}
|
|
864
|
+
if (receipt.designRevision?.triggered) {
|
|
865
|
+
console.log(`${styleSystemPrefix()} ${dim("design pass")} ${yellow("auto-revised")} ${receipt.designRevision.initialSummary || ""}`.trim());
|
|
866
|
+
}
|
|
864
867
|
}
|
|
865
868
|
|
|
866
869
|
function printImpactMap(impact) {
|
|
@@ -5384,6 +5387,9 @@ async function promptLoop(agent, session, context) {
|
|
|
5384
5387
|
const vc = v === "strong" ? green(v) : v === "weak" ? red(v) : yellow(v);
|
|
5385
5388
|
lines.push(`${dim("design:")} ${vc} — ${buildResult.designReview.summary}`);
|
|
5386
5389
|
}
|
|
5390
|
+
if (buildResult.designRevision?.triggered) {
|
|
5391
|
+
lines.push(`${dim("design pass:")} ${yellow("auto-revised")} — ${buildResult.designRevision.initialSummary || "first pass revised"}`);
|
|
5392
|
+
}
|
|
5387
5393
|
|
|
5388
5394
|
// Task state
|
|
5389
5395
|
lines.push(`${dim("task:")} ${task.name} → ${cyan("review-ready")}`);
|
package/src/frontend.js
CHANGED
|
@@ -47,6 +47,50 @@ const AUDIENCE_HINTS = [
|
|
|
47
47
|
["operators", /\b(founders?|operators?|engineers?|designers?)\b/i]
|
|
48
48
|
];
|
|
49
49
|
|
|
50
|
+
const ARCHETYPE_RULES = {
|
|
51
|
+
"editorial-minimal": [
|
|
52
|
+
"Use restrained editorial hierarchy with fewer, larger blocks of content.",
|
|
53
|
+
"Favor margins, type rhythm, and restraint over decorative UI.",
|
|
54
|
+
"Keep the palette quiet and avoid loud CTA-heavy marketing patterns."
|
|
55
|
+
],
|
|
56
|
+
"luxury-magazine": [
|
|
57
|
+
"Use strong type contrast and asymmetry, but avoid fake prestige or fashion cliché overload.",
|
|
58
|
+
"Make one or two sections do the visual heavy lifting instead of many repetitive cards.",
|
|
59
|
+
"Let spacing and composition carry the premium feel more than gradients or gimmicks."
|
|
60
|
+
],
|
|
61
|
+
"founder-journal": [
|
|
62
|
+
"Keep the voice direct and personal rather than generic lifestyle-editorial.",
|
|
63
|
+
"Use sparse structure and strong notebook-like pacing.",
|
|
64
|
+
"Avoid fake magazine tropes and unnecessary promotional UI."
|
|
65
|
+
],
|
|
66
|
+
"brutalist-culture": [
|
|
67
|
+
"Prioritize bold hierarchy, high contrast, and deliberate rawness.",
|
|
68
|
+
"Use fewer colors and stronger typographic tension.",
|
|
69
|
+
"Avoid soft premium-blog aesthetics."
|
|
70
|
+
],
|
|
71
|
+
"high-contrast-tech": [
|
|
72
|
+
"Use a colder palette, harder edges, and precise spacing.",
|
|
73
|
+
"Prefer technical clarity over lifestyle editorial softness.",
|
|
74
|
+
"Avoid generic startup landing-page sections unless they are explicitly requested."
|
|
75
|
+
],
|
|
76
|
+
"quiet-portfolio": [
|
|
77
|
+
"Let work and case-study structure carry the page, not decorative chrome.",
|
|
78
|
+
"Use calm spacing and image framing with minimal interface noise.",
|
|
79
|
+
"Avoid blog-style editorial filler."
|
|
80
|
+
]
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const SLOP_PATTERNS = [
|
|
84
|
+
{ key: "placeholder_images", label: "placeholder image service", pattern: /\b(?:picsum\.photos|placehold\.co|placeholder\.com)\b/i, weight: 3 },
|
|
85
|
+
{ key: "tailwind_cdn", label: "Tailwind CDN starter styling", pattern: /cdn\.tailwindcss\.com/i, weight: 2 },
|
|
86
|
+
{ key: "inter_playfair", label: "generic Inter/Playfair premium pairing", pattern: /Inter|Playfair\s+Display/i, weight: 2 },
|
|
87
|
+
{ key: "fake_prestige", label: "fake prestige or publication badge", pattern: /\b(?:featured in|as seen in|forbes|the atlantic|wall street journal|award-winning)\b/i, weight: 3 },
|
|
88
|
+
{ key: "fake_founder_lore", label: "fake founder or studio lore", pattern: /\b(?:founded in|est\s+20\d{2}|from the studio|published from a small studio|founder\s*&\s*essayist)\b/i, weight: 2 },
|
|
89
|
+
{ key: "newsletter_cliche", label: "generic newsletter promise copy", pattern: /\b(?:no spam, ever|respect your inbox|join the newsletter|subscribe to the journal)\b/i, weight: 1 },
|
|
90
|
+
{ key: "fake_ui_chrome", label: "fake low-value UI chrome", pattern: /\b(?:search|filterCategory|showPostModal|toggleSearch|Latest Stories|Recent Dispatches)\b/i, weight: 1 },
|
|
91
|
+
{ key: "premium_blog_trope", label: "generic premium-blog editorial trope", pattern: /\b(?:thoughtful living|slow living|curated reflections|crafted with intention|made with intention)\b/i, weight: 2 }
|
|
92
|
+
];
|
|
93
|
+
|
|
50
94
|
function normalizeContent(content) {
|
|
51
95
|
if (typeof content === "string") return content;
|
|
52
96
|
if (Array.isArray(content)) {
|
|
@@ -127,7 +171,8 @@ export function buildFrontendExecutionContext({ promptText = "", profile = "code
|
|
|
127
171
|
"Prefer hand-authored CSS variables and layout rules over generic template utility sprawl when feasible.",
|
|
128
172
|
"Cut fake credibility elements, fake brands, fake testimonials, and filler interface chrome unless explicitly requested.",
|
|
129
173
|
"Avoid placeholder image services, Inter/Playfair default pairings, Tailwind CDN starter aesthetics, and generic premium-blog tropes.",
|
|
130
|
-
"Prefer fewer sections with stronger hierarchy over a long page full of low-value widgets."
|
|
174
|
+
"Prefer fewer sections with stronger hierarchy over a long page full of low-value widgets.",
|
|
175
|
+
...(ARCHETYPE_RULES[archetype] || [])
|
|
131
176
|
].join("\n");
|
|
132
177
|
|
|
133
178
|
return {
|
|
@@ -156,6 +201,61 @@ export function shouldRunFrontendReview({ promptText = "", receipt = null, profi
|
|
|
156
201
|
return changedFiles.some((filePath) => isFrontendFile(filePath));
|
|
157
202
|
}
|
|
158
203
|
|
|
204
|
+
export function detectFrontendSlop({ promptText = "", assistantText = "", receipt = null, designReview = null } = {}) {
|
|
205
|
+
const haystack = [
|
|
206
|
+
String(promptText || ""),
|
|
207
|
+
String(assistantText || ""),
|
|
208
|
+
String(receipt?.diff || ""),
|
|
209
|
+
Array.isArray(receipt?.changedFiles) ? receipt.changedFiles.join("\n") : ""
|
|
210
|
+
].join("\n");
|
|
211
|
+
const flags = [];
|
|
212
|
+
let score = 0;
|
|
213
|
+
for (const item of SLOP_PATTERNS) {
|
|
214
|
+
if (item.pattern.test(haystack)) {
|
|
215
|
+
flags.push(item.label);
|
|
216
|
+
score += item.weight;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (designReview?.verdict === "weak") score += 3;
|
|
220
|
+
else if (designReview?.verdict === "caution") score += 1;
|
|
221
|
+
return {
|
|
222
|
+
score,
|
|
223
|
+
flags,
|
|
224
|
+
severe: score >= 5,
|
|
225
|
+
summary: flags.length > 0 ? `frontend slop flags: ${flags.join(", ")}` : "no deterministic frontend slop flags"
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function shouldAutoReviseFrontend({ designReview = null, slop = null, revisionCount = 0 } = {}) {
|
|
230
|
+
if (revisionCount >= 1) return false;
|
|
231
|
+
if (!designReview) return false;
|
|
232
|
+
if (designReview.verdict === "weak") return true;
|
|
233
|
+
if (designReview.verdict === "caution" && (slop?.score || 0) >= 3) return true;
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function buildFrontendRevisionPrompt({
|
|
238
|
+
originalPrompt = "",
|
|
239
|
+
designReview = null,
|
|
240
|
+
slop = null
|
|
241
|
+
} = {}) {
|
|
242
|
+
const issues = Array.isArray(designReview?.issues) ? designReview.issues.slice(0, 6) : [];
|
|
243
|
+
const nextPass = Array.isArray(designReview?.nextPass) ? designReview.nextPass.slice(0, 6) : [];
|
|
244
|
+
const slopFlags = Array.isArray(slop?.flags) ? slop.flags.slice(0, 6) : [];
|
|
245
|
+
const blocks = [
|
|
246
|
+
`Revise the generated frontend to address the design problems from the first pass.`,
|
|
247
|
+
`Original task: ${String(originalPrompt || "").trim()}`,
|
|
248
|
+
issues.length > 0 ? `Problems to fix:\n- ${issues.join("\n- ")}` : "",
|
|
249
|
+
slopFlags.length > 0 ? `Deterministic slop flags:\n- ${slopFlags.join("\n- ")}` : "",
|
|
250
|
+
nextPass.length > 0 ? `Revision priorities:\n- ${nextPass.join("\n- ")}` : "",
|
|
251
|
+
"Do not add new filler sections.",
|
|
252
|
+
"Do not add fake prestige, fake testimonials, fake brands, or placeholder-image services.",
|
|
253
|
+
"Simplify the page if needed. Stronger direction with fewer elements is preferred over busier generic output.",
|
|
254
|
+
"Rewrite the weakest sections rather than making superficial tweaks."
|
|
255
|
+
].filter(Boolean);
|
|
256
|
+
return blocks.join("\n\n");
|
|
257
|
+
}
|
|
258
|
+
|
|
159
259
|
function normalizeFrontendReview(review) {
|
|
160
260
|
const verdict = String(review?.verdict || "caution").trim().toLowerCase();
|
|
161
261
|
return {
|
package/src/workflow.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { createTask, findTaskByName, saveTask, slugify } from "./task-store.js";
|
|
2
2
|
import { computeImpactMap, summarizeImpactMap } from "./impact.js";
|
|
3
3
|
import { reviewTurn, challengeReceipt } from "./reviewer.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
buildFrontendExecutionContext,
|
|
6
|
+
buildFrontendRevisionPrompt,
|
|
7
|
+
detectFrontendSlop,
|
|
8
|
+
reviewFrontendTurn,
|
|
9
|
+
shouldAutoReviseFrontend,
|
|
10
|
+
shouldRunFrontendReview
|
|
11
|
+
} from "./frontend.js";
|
|
5
12
|
import { runPlannerPass, formatPlanForExecutor, formatPlanForDisplay } from "./planner.js";
|
|
6
13
|
|
|
7
14
|
export async function runBuildWorkflow({
|
|
@@ -78,70 +85,114 @@ export async function runBuildWorkflow({
|
|
|
78
85
|
: promptText;
|
|
79
86
|
|
|
80
87
|
// Run the turn
|
|
81
|
-
|
|
88
|
+
let response = await agent.runBuildTurn(executorPrompt, handlers);
|
|
82
89
|
|
|
83
90
|
// Complete turn and get receipt
|
|
84
|
-
|
|
91
|
+
let receipt = await agent.toolRuntime.completeTurn({ signal: handlers.signal });
|
|
85
92
|
|
|
86
93
|
if (!receipt) {
|
|
87
94
|
return { response, receipt: null, impact: null, review: null };
|
|
88
95
|
}
|
|
89
96
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Run sentinel review
|
|
102
|
-
let review = null;
|
|
103
|
-
if (receipt.mutated && context.runtime.reviewer?.enabled !== false) {
|
|
104
|
-
try {
|
|
105
|
-
review = await reviewTurn({
|
|
106
|
-
apiKey: context.runtime.apiKey,
|
|
107
|
-
baseUrl: context.runtime.baseUrl,
|
|
108
|
-
model: context.runtime.reviewer?.model || agent.getModel(),
|
|
109
|
-
promptText,
|
|
110
|
-
assistantText: response.content || "",
|
|
111
|
-
receipt: { ...receipt, diff: receipt.diff || "" },
|
|
112
|
-
impact,
|
|
113
|
-
maxDiffChars: context.runtime.reviewer?.maxDiffChars,
|
|
114
|
-
signal: handlers.signal
|
|
97
|
+
async function analyze(activeReceipt, activeResponse) {
|
|
98
|
+
let impact = null;
|
|
99
|
+
if (activeReceipt.mutated && context.runtime.impact?.enabled !== false) {
|
|
100
|
+
impact = await computeImpactMap({
|
|
101
|
+
cwd: context.cwd,
|
|
102
|
+
changedFiles: activeReceipt.changedFiles || [],
|
|
103
|
+
maxRelated: context.runtime.impact?.maxRelated,
|
|
104
|
+
maxTests: context.runtime.impact?.maxTests
|
|
115
105
|
});
|
|
116
|
-
} catch (error) {
|
|
117
|
-
review = {
|
|
118
|
-
verdict: "caution",
|
|
119
|
-
summary: `review failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
120
|
-
concerns: ["Sentinel reviewer could not complete."],
|
|
121
|
-
followups: []
|
|
122
|
-
};
|
|
123
106
|
}
|
|
107
|
+
|
|
108
|
+
let review = null;
|
|
109
|
+
if (activeReceipt.mutated && context.runtime.reviewer?.enabled !== false) {
|
|
110
|
+
try {
|
|
111
|
+
review = await reviewTurn({
|
|
112
|
+
apiKey: context.runtime.apiKey,
|
|
113
|
+
baseUrl: context.runtime.baseUrl,
|
|
114
|
+
model: context.runtime.reviewer?.model || agent.getModel(),
|
|
115
|
+
promptText,
|
|
116
|
+
assistantText: activeResponse.content || "",
|
|
117
|
+
receipt: { ...activeReceipt, diff: activeReceipt.diff || "" },
|
|
118
|
+
impact,
|
|
119
|
+
maxDiffChars: context.runtime.reviewer?.maxDiffChars,
|
|
120
|
+
signal: handlers.signal
|
|
121
|
+
});
|
|
122
|
+
} catch (error) {
|
|
123
|
+
review = {
|
|
124
|
+
verdict: "caution",
|
|
125
|
+
summary: `review failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
126
|
+
concerns: ["Sentinel reviewer could not complete."],
|
|
127
|
+
followups: []
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let designReview = null;
|
|
133
|
+
if (shouldRunFrontendReview({ promptText, receipt: activeReceipt, profile: agent.getProfile() })) {
|
|
134
|
+
try {
|
|
135
|
+
designReview = await reviewFrontendTurn({
|
|
136
|
+
apiKey: context.runtime.apiKey,
|
|
137
|
+
baseUrl: context.runtime.baseUrl,
|
|
138
|
+
model: context.runtime.reviewer?.model || agent.getModel(),
|
|
139
|
+
promptText,
|
|
140
|
+
assistantText: activeResponse.content || "",
|
|
141
|
+
receipt: { ...activeReceipt, diff: activeReceipt.diff || "" },
|
|
142
|
+
signal: handlers.signal
|
|
143
|
+
});
|
|
144
|
+
} catch (error) {
|
|
145
|
+
designReview = {
|
|
146
|
+
verdict: "caution",
|
|
147
|
+
summary: `design review failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
148
|
+
strengths: [],
|
|
149
|
+
issues: ["Frontend design reviewer could not complete."],
|
|
150
|
+
nextPass: []
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const designSlop = designReview
|
|
156
|
+
? detectFrontendSlop({ promptText, assistantText: activeResponse.content || "", receipt: activeReceipt, designReview })
|
|
157
|
+
: null;
|
|
158
|
+
|
|
159
|
+
return { impact, review, designReview, designSlop };
|
|
124
160
|
}
|
|
125
161
|
|
|
126
|
-
let designReview =
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
162
|
+
let { impact, review, designReview, designSlop } = await analyze(receipt, response);
|
|
163
|
+
let designRevision = null;
|
|
164
|
+
|
|
165
|
+
if (shouldAutoReviseFrontend({ designReview, slop: designSlop, revisionCount: 0 })) {
|
|
166
|
+
const firstPassVerdict = designReview?.verdict || null;
|
|
167
|
+
const firstPassSummary = String(designReview?.summary || "").trim();
|
|
168
|
+
const firstPassSlopFlags = Array.isArray(designSlop?.flags) ? [...designSlop.flags] : [];
|
|
169
|
+
const revisionPrompt = buildFrontendRevisionPrompt({
|
|
170
|
+
originalPrompt: promptText,
|
|
171
|
+
designReview,
|
|
172
|
+
slop: designSlop
|
|
173
|
+
});
|
|
174
|
+
const revisionCtx = {
|
|
175
|
+
...executionCtx,
|
|
176
|
+
phase: "design-revision",
|
|
177
|
+
reminders: [
|
|
178
|
+
executionCtx.reminders || "",
|
|
179
|
+
"Automatic second pass: fix the flagged frontend design issues without widening scope."
|
|
180
|
+
].filter(Boolean).join("\n")
|
|
181
|
+
};
|
|
182
|
+
agent.setExecutionContext(revisionCtx);
|
|
183
|
+
if (task.activeContract) {
|
|
184
|
+
agent.toolRuntime.setCurrentContract(task.activeContract);
|
|
185
|
+
}
|
|
186
|
+
response = await agent.runBuildTurn(revisionPrompt, handlers);
|
|
187
|
+
const revisedReceipt = await agent.toolRuntime.completeTurn({ signal: handlers.signal });
|
|
188
|
+
if (revisedReceipt) {
|
|
189
|
+
receipt = revisedReceipt;
|
|
190
|
+
({ impact, review, designReview, designSlop } = await analyze(receipt, response));
|
|
191
|
+
designRevision = {
|
|
192
|
+
triggered: true,
|
|
193
|
+
firstPassVerdict,
|
|
194
|
+
initialSummary: firstPassSummary,
|
|
195
|
+
slopFlags: firstPassSlopFlags
|
|
145
196
|
};
|
|
146
197
|
}
|
|
147
198
|
}
|
|
@@ -151,6 +202,8 @@ export async function runBuildWorkflow({
|
|
|
151
202
|
if (impact) updates.impact = impact;
|
|
152
203
|
if (review) updates.review = review;
|
|
153
204
|
if (designReview) updates.designReview = designReview;
|
|
205
|
+
if (designSlop) updates.designSlop = designSlop;
|
|
206
|
+
if (designRevision) updates.designRevision = designRevision;
|
|
154
207
|
let finalReceipt = receipt;
|
|
155
208
|
if (Object.keys(updates).length > 0) {
|
|
156
209
|
finalReceipt = await agent.toolRuntime.updateReceipt(receipt.id, updates) || receipt;
|