@tritard/waterbrother 0.8.27 → 0.8.29

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.27",
3
+ "version": "0.8.29",
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
@@ -606,6 +606,50 @@ function buildSyntheticAssistantOutput(receipt) {
606
606
  return null;
607
607
  }
608
608
 
609
+ function hasFrontendCodeEcho(text) {
610
+ const body = String(text || "");
611
+ return /```(?:html|css|js|javascript|jsx|tsx)?[\s\S]{120,}```/i.test(body) || /<!DOCTYPE html>/i.test(body);
612
+ }
613
+
614
+ function hasWriteMutationTool(receipt) {
615
+ const tools = Array.isArray(receipt?.tools) ? receipt.tools : [];
616
+ return tools.some((tool) => ["write_file", "replace_in_file", "apply_patch"].includes(tool?.name));
617
+ }
618
+
619
+ function deriveContractWriteTarget(contract) {
620
+ const paths = Array.isArray(contract?.paths) ? contract.paths : [];
621
+ for (const item of paths) {
622
+ const raw = String(item || "").trim();
623
+ if (!raw) continue;
624
+ const normalized = raw.replace(/\/\*\*?$/g, "").replace(/\*+$/g, "");
625
+ if (normalized) return normalized;
626
+ }
627
+ return null;
628
+ }
629
+
630
+ function shouldRecoverFrontendCodeEcho({ frontendExecutionContext, receipt, assistantText }) {
631
+ if (!frontendExecutionContext?.frontend) return false;
632
+ if (!receipt || receipt.mutated) return false;
633
+ if (!hasFrontendCodeEcho(assistantText)) return false;
634
+ if (hasWriteMutationTool(receipt)) return false;
635
+ const tools = Array.isArray(receipt.tools) ? receipt.tools : [];
636
+ return tools.some((tool) => tool?.name === "declare_contract") || tools.some((tool) => tool?.name === "make_directory");
637
+ }
638
+
639
+ function buildFrontendWriteRecoveryPrompt({ originalPrompt, contract }) {
640
+ const target = deriveContractWriteTarget(contract);
641
+ const lines = [
642
+ "You already planned the work but printed code into chat instead of writing files.",
643
+ `Original task: ${String(originalPrompt || "").trim()}`,
644
+ target ? `Write the generated frontend into the declared scope now: ${target}` : "Write the generated frontend into the declared contract scope now.",
645
+ "Do not print long code blocks in chat.",
646
+ "Use write_file (or replace_in_file/apply_patch if needed) to create the actual site files.",
647
+ "If this is a new site in a folder, default to writing index.html there unless multiple files are clearly justified.",
648
+ "After writing, reply briefly with only the files created or updated."
649
+ ];
650
+ return lines.join("\n\n");
651
+ }
652
+
609
653
  function color256(fg, text) {
610
654
  return `\x1b[38;5;${fg}m${text}\x1b[0m`;
611
655
  }
@@ -3576,6 +3620,40 @@ async function runTextTurnInteractive({
3576
3620
  precomputedReceipt = await agent.toolRuntime.completeTurn({ signal: abortController?.signal });
3577
3621
  renderedAssistantText = buildSyntheticAssistantOutput(precomputedReceipt) || renderedAssistantText;
3578
3622
  }
3623
+ if (!precomputedReceipt && frontendExecutionContext) {
3624
+ const candidateReceipt = await agent.toolRuntime.completeTurn({ signal: abortController?.signal });
3625
+ if (shouldRecoverFrontendCodeEcho({ frontendExecutionContext, receipt: candidateReceipt, assistantText: response.content || "" })) {
3626
+ const recoverySpinner = createProgressSpinner("writing files...");
3627
+ printLiveTrace("frontend recovery: assistant echoed code, retrying with write_file", context.runtime.traceMode);
3628
+ agent.toolRuntime.setReadOnlyRoots(readOnlyRoots);
3629
+ agent.toolRuntime.setWriteRoots(writeRoots);
3630
+ if (frontendExecutionContext) {
3631
+ const merged = { ...(previousExecutionContext || {}), ...frontendExecutionContext };
3632
+ if (previousExecutionContext?.reminders && frontendExecutionContext.reminders) {
3633
+ merged.reminders = `${previousExecutionContext.reminders}\n${frontendExecutionContext.reminders}`;
3634
+ }
3635
+ agent.setExecutionContext(merged);
3636
+ }
3637
+ try {
3638
+ response = await agent.runTurn(buildFrontendWriteRecoveryPrompt({ originalPrompt: effectivePromptText, contract: candidateReceipt?.contract }), {
3639
+ signal: abortController?.signal,
3640
+ onStateChange(state) {
3641
+ printLiveTrace(`state=${state}`, context.runtime.traceMode, { verboseOnly: true });
3642
+ }
3643
+ });
3644
+ renderedAssistantText = response.content || "";
3645
+ } finally {
3646
+ recoverySpinner.stop();
3647
+ agent.toolRuntime.setReadOnlyRoots([]);
3648
+ agent.toolRuntime.setWriteRoots([]);
3649
+ if (frontendExecutionContext) {
3650
+ agent.setExecutionContext(previousExecutionContext);
3651
+ }
3652
+ }
3653
+ } else {
3654
+ precomputedReceipt = candidateReceipt;
3655
+ }
3656
+ }
3579
3657
  printAssistantOutput(renderedAssistantText);
3580
3658
  await setSessionRunState(currentSession, agent, "done");
3581
3659
  printTurnSummary(turnSummary, response, { modelId: agent.getModel(), costTracker: context.costTracker, traceMode: context.runtime.traceMode });
package/src/frontend.js CHANGED
@@ -69,14 +69,16 @@ const BENCHMARK_SITE_TYPE_RULES = {
69
69
  blog: [
70
70
  "Benchmark blog mode: use neutral structural placeholders or concrete subject matter instead of publication worldbuilding or reflective-editorial atmosphere prose.",
71
71
  "Benchmark blog mode: do not wrap the page in a generic publication shell like Journal, Featured Essay, Latest Dispatches, Print Edition, Submit Work, or similar magazine-site framing.",
72
- "Benchmark blog mode: avoid the default editorial scaffold of hero, story list, archive rail, topics grid, and publication footer unless the user explicitly asked for a magazine-style site."
72
+ "Benchmark blog mode: avoid the default editorial scaffold of hero, story list, archive rail, topics grid, and publication footer unless the user explicitly asked for a magazine-style site.",
73
+ "Benchmark blog mode: do not use Tailwind CDN starter theming, picsum/placehold imagery, or fake keyboard/search chrome."
73
74
  ],
74
75
  store: [
75
76
  "Benchmark ecommerce mode: prioritize conversion architecture over editorial styling. The page should sell, not just look clean.",
76
77
  "Benchmark ecommerce mode: include proof, objection handling, and trust signals that belong on a real single-product PDP.",
77
78
  "Benchmark ecommerce mode: do not leave the main merchandising surface as a literal placeholder or demo box.",
78
79
  "Benchmark ecommerce mode: do not use Tailwind CDN starter theming or placeholder product images such as picsum/placehold on the live merchandising surface.",
79
- "Benchmark ecommerce mode: fake command palettes, fake app shortcuts, and unrelated theme chrome are disallowed."
80
+ "Benchmark ecommerce mode: fake command palettes, fake app shortcuts, and unrelated theme chrome are disallowed.",
81
+ "Benchmark ecommerce mode: fake review counts, fake bestseller labels, and fake as-featured-in proof are disallowed."
80
82
  ]
81
83
  };
82
84
 
@@ -440,6 +442,10 @@ export function detectFrontendSlop({ promptText = "", assistantText = "", receip
440
442
  flags.push("benchmark store used placeholder product imagery");
441
443
  score += 4;
442
444
  }
445
+ if (siteType === "store" && benchmarkMode && /\b(?:best seller|best seller|\d[\d,]*\s+reviews|as featured in|featured in|trusted by|studio engineers approved)\b/i.test(haystack)) {
446
+ flags.push("benchmark store used fake proof or badge chrome");
447
+ score += 4;
448
+ }
443
449
  if (siteType === "store" && benchmarkMode && !/\b(?:review|reviews|testimonial|rated|stars?|customers?)\b/i.test(haystack)) {
444
450
  flags.push("benchmark store lacks social proof or review architecture");
445
451
  score += 2;
@@ -460,6 +466,14 @@ export function detectFrontendSlop({ promptText = "", assistantText = "", receip
460
466
  flags.push("benchmark blog used generic publication-shell framing");
461
467
  score += 3;
462
468
  }
469
+ if (siteType === "blog" && benchmarkMode && /cdn\.tailwindcss\.com/i.test(haystack)) {
470
+ flags.push("benchmark blog relied on Tailwind CDN starter theming");
471
+ score += 4;
472
+ }
473
+ if (siteType === "blog" && benchmarkMode && /\b(?:picsum\.photos|placehold\.co|placeholder\.com)\b/i.test(haystack)) {
474
+ flags.push("benchmark blog used placeholder imagery");
475
+ score += 4;
476
+ }
463
477
  if (/\b(?:command palette would open here|metaKey && e\.key === ['"]k['"]|keyboard accessibility)\b/i.test(haystack)) {
464
478
  flags.push("fake keyboard or command-palette gimmick");
465
479
  score += 3;
@@ -478,7 +492,7 @@ export function detectFrontendSlop({ promptText = "", assistantText = "", receip
478
492
  return {
479
493
  score,
480
494
  flags,
481
- hardBlock: flags.some((flag) => /fictional publication identity|generic publication-shell framing|fake keyboard|reflective-editorial atmosphere|primary merchandising surface as a placeholder|Tailwind CDN starter theming|placeholder product imagery/.test(flag)),
495
+ hardBlock: flags.some((flag) => /fictional publication identity|generic publication-shell framing|fake keyboard|reflective-editorial atmosphere|primary merchandising surface as a placeholder|Tailwind CDN starter theming|placeholder product imagery|placeholder imagery|fake proof or badge chrome/.test(flag)),
482
496
  severe: score >= 5,
483
497
  summary: flags.length > 0 ? `frontend slop flags: ${flags.join(", ")}` : "no deterministic frontend slop flags"
484
498
  };
@@ -520,8 +534,10 @@ export function buildFrontendRevisionPrompt({
520
534
  "Cut reflective-editorial filler copy and replace it with either concrete language or neutral structural placeholders.",
521
535
  "For benchmark blog tasks, default to neutral structural placeholder content instead of invented publication framing, issue metadata, or named contributors.",
522
536
  "For benchmark blog tasks, do not use generic publication-shell labels like Journal, Featured Essay, Latest Dispatches, Print Edition, Submit Work, or publication-footer framing.",
537
+ "For benchmark blog tasks, do not use Tailwind CDN starter theming, picsum-style placeholder imagery, or fake search/shortcut chrome.",
523
538
  "For benchmark store tasks, do not leave the product image area as a labeled placeholder. Use product-shaped merchandising composition, proof, and objection-handling blocks instead.",
524
539
  "For benchmark store tasks, do not use Tailwind CDN starter theming, picsum-style placeholder imagery, or fake command-palette behavior.",
540
+ "For benchmark store tasks, do not invent review counts, bestseller labels, or as-featured-in proof unless the user explicitly requested fictional marketing chrome.",
525
541
  "Reduce section count if needed and push one stronger asymmetrical composition instead of a sequence of balanced blocks.",
526
542
  "Simplify the page if needed. Stronger direction with fewer elements is preferred over busier generic output.",
527
543
  "Rewrite the weakest sections rather than making superficial tweaks."
package/src/tools.js CHANGED
@@ -1037,12 +1037,23 @@ export function createToolRuntime({
1037
1037
  return { decision: "ask", reason: "No matching allow rule" };
1038
1038
  }
1039
1039
 
1040
+ function getContractWriteRoots() {
1041
+ const rawPaths = Array.isArray(currentTurn.contract?.paths) ? currentTurn.contract.paths : [];
1042
+ return normalizePathList(
1043
+ rawPaths
1044
+ .map((item) => String(item || "").trim())
1045
+ .filter(Boolean)
1046
+ .map((item) => item.replace(/\/\*\*?$/g, "").replace(/\*+$/g, ""))
1047
+ .filter(Boolean)
1048
+ );
1049
+ }
1050
+
1040
1051
  function contractAllows(toolName, args = {}) {
1041
1052
  if (!currentTurn.contract) return { ok: !toolRequiresContract(toolName, args), reason: "No active contract" };
1042
1053
  if ((toolName === "write_file" || toolName === "replace_in_file" || toolName === "make_directory" || toolName === "delete_path") && args.path) {
1043
1054
  try {
1044
1055
  resolveSandboxPath(cwd, args.path, allowOutsideCwd, {
1045
- allowedWriteRoots: getCurrentWriteRoots()
1056
+ allowedWriteRoots: [...getCurrentWriteRoots(), ...getContractWriteRoots()]
1046
1057
  });
1047
1058
  } catch {
1048
1059
  const touchedPaths = getTouchedPathsForTool(toolName, args);