@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.8.13",
3
+ "version": "0.8.14",
4
4
  "description": "Waterbrother: Grok-powered coding CLI with local tools, sessions, operator modes, and approval controls",
5
5
  "type": "module",
6
6
  "bin": {
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 { buildFrontendExecutionContext, reviewFrontendTurn, shouldRunFrontendReview } from "./frontend.js";
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
- const response = await agent.runBuildTurn(executorPrompt, handlers);
88
+ let response = await agent.runBuildTurn(executorPrompt, handlers);
82
89
 
83
90
  // Complete turn and get receipt
84
- const receipt = await agent.toolRuntime.completeTurn({ signal: handlers.signal });
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
- // Run impact analysis
91
- let impact = null;
92
- if (receipt.mutated && context.runtime.impact?.enabled !== false) {
93
- impact = await computeImpactMap({
94
- cwd: context.cwd,
95
- changedFiles: receipt.changedFiles || [],
96
- maxRelated: context.runtime.impact?.maxRelated,
97
- maxTests: context.runtime.impact?.maxTests
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 = null;
127
- if (shouldRunFrontendReview({ promptText, receipt, profile: agent.getProfile() })) {
128
- try {
129
- designReview = await reviewFrontendTurn({
130
- apiKey: context.runtime.apiKey,
131
- baseUrl: context.runtime.baseUrl,
132
- model: context.runtime.reviewer?.model || agent.getModel(),
133
- promptText,
134
- assistantText: response.content || "",
135
- receipt: { ...receipt, diff: receipt.diff || "" },
136
- signal: handlers.signal
137
- });
138
- } catch (error) {
139
- designReview = {
140
- verdict: "caution",
141
- summary: `design review failed: ${error instanceof Error ? error.message : String(error)}`,
142
- strengths: [],
143
- issues: ["Frontend design reviewer could not complete."],
144
- nextPass: []
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;