@tritard/waterbrother 0.14.21 → 0.15.0

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.14.21",
3
+ "version": "0.15.0",
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/scorecard.js CHANGED
@@ -4,11 +4,25 @@ import crypto from "node:crypto";
4
4
 
5
5
  const MAX_INDEX_ENTRIES = 200;
6
6
  const MAX_CALIBRATION_CHARS = 2000;
7
+ const MAX_GLOBAL_CALIBRATION_CHARS = 800;
7
8
 
8
9
  function scorecardsDir(cwd) {
9
10
  return path.join(cwd, ".waterbrother", "memory", "scorecards");
10
11
  }
11
12
 
13
+ function globalScorecardsDir() {
14
+ const home = process.env.HOME || process.env.USERPROFILE || "";
15
+ return path.join(home, ".waterbrother", "global-scorecards");
16
+ }
17
+
18
+ function globalIndexPath() {
19
+ return path.join(globalScorecardsDir(), "index.json");
20
+ }
21
+
22
+ function globalScorecardPath(id) {
23
+ return path.join(globalScorecardsDir(), `${id}.json`);
24
+ }
25
+
12
26
  function indexPath(cwd) {
13
27
  return path.join(scorecardsDir(cwd), "index.json");
14
28
  }
@@ -285,11 +299,12 @@ async function writeIndex(cwd, index) {
285
299
  }
286
300
 
287
301
  export async function saveScorecard({ cwd, scorecard }) {
302
+ // Save to project
288
303
  await fs.mkdir(scorecardsDir(cwd), { recursive: true });
289
304
  await fs.writeFile(scorecardPath(cwd, scorecard.id), `${JSON.stringify(scorecard, null, 2)}\n`, "utf8");
290
305
 
291
306
  const index = await readIndex(cwd);
292
- index.unshift({
307
+ const entry = {
293
308
  id: scorecard.id,
294
309
  taskName: scorecard.taskName,
295
310
  scope: scorecard.scope,
@@ -297,9 +312,24 @@ export async function saveScorecard({ cwd, scorecard }) {
297
312
  composite: scorecard.scores.composite,
298
313
  precision: scorecard.precision,
299
314
  timestamp: scorecard.timestamp
300
- });
315
+ };
316
+ index.unshift(entry);
301
317
  if (index.length > MAX_INDEX_ENTRIES) index.length = MAX_INDEX_ENTRIES;
302
318
  await writeIndex(cwd, index);
319
+
320
+ // Save to global store (cross-project learning)
321
+ try {
322
+ const gDir = globalScorecardsDir();
323
+ await fs.mkdir(gDir, { recursive: true });
324
+ await fs.writeFile(globalScorecardPath(scorecard.id), `${JSON.stringify(scorecard, null, 2)}\n`, "utf8");
325
+
326
+ let gIndex = [];
327
+ try { gIndex = JSON.parse(await fs.readFile(globalIndexPath(), "utf8")); } catch {}
328
+ if (!Array.isArray(gIndex)) gIndex = [];
329
+ gIndex.unshift({ ...entry, project: path.basename(cwd) });
330
+ if (gIndex.length > MAX_INDEX_ENTRIES) gIndex.length = MAX_INDEX_ENTRIES;
331
+ await fs.writeFile(globalIndexPath(), `${JSON.stringify(gIndex, null, 2)}\n`, "utf8");
332
+ } catch {}
303
333
  }
304
334
 
305
335
  export async function findRelevantScorecards({ cwd, filePatterns = [], limit = 10 }) {
@@ -369,6 +399,94 @@ export function suggestAutonomyForScope(scorecards) {
369
399
  return "scoped";
370
400
  }
371
401
 
402
+ // --- Global scorecards (cross-project learning) ---
403
+
404
+ export async function loadGlobalScorecards({ limit = 20 } = {}) {
405
+ const gDir = globalScorecardsDir();
406
+ try {
407
+ const raw = await fs.readFile(path.join(gDir, "index.json"), "utf8");
408
+ const index = JSON.parse(raw);
409
+ if (!Array.isArray(index)) return [];
410
+ const cards = [];
411
+ for (const entry of index.slice(0, limit)) {
412
+ try {
413
+ const data = await fs.readFile(globalScorecardPath(entry.id), "utf8");
414
+ cards.push({ ...JSON.parse(data), _project: entry.project });
415
+ } catch {}
416
+ }
417
+ return cards;
418
+ } catch {
419
+ return [];
420
+ }
421
+ }
422
+
423
+ export function buildGlobalCalibrationBlock(globalCards) {
424
+ if (!globalCards || globalCards.length < 3) return "";
425
+
426
+ const lines = ["Cross-project patterns (from your build history):"];
427
+ let chars = lines[0].length;
428
+
429
+ // Aggregate stats
430
+ const totalBuilds = globalCards.length;
431
+ const avgComposite = globalCards.reduce((s, c) => s + (c.scores?.composite || 0), 0) / totalBuilds;
432
+
433
+ // Find recurring blind spots across projects
434
+ const allFindings = {};
435
+ const allConcerns = {};
436
+ const attrSums = { plan: 0, execution: 0, verification: 0, sentinel: 0 };
437
+ const attrCounts = { plan: 0, execution: 0, verification: 0, sentinel: 0 };
438
+
439
+ for (const sc of globalCards) {
440
+ for (const f of (sc.outcomes?.quality?.findings || [])) {
441
+ allFindings[f] = (allFindings[f] || 0) + 1;
442
+ }
443
+ for (const c of (sc.outcomes?.sentinel?.concerns || [])) {
444
+ // Generalize concern — strip file-specific details
445
+ const generic = c.replace(/[`'"]\S+[`'"]/g, "...").replace(/\b\w+\.(js|ts|py|jsx|tsx|vue)\b/g, "...");
446
+ allConcerns[generic] = (allConcerns[generic] || 0) + 1;
447
+ }
448
+ if (sc.attribution) {
449
+ for (const key of Object.keys(attrSums)) {
450
+ if (sc.attribution[key] !== null && sc.attribution[key] !== undefined) {
451
+ attrSums[key] += sc.attribution[key];
452
+ attrCounts[key]++;
453
+ }
454
+ }
455
+ }
456
+ }
457
+
458
+ const line1 = `${totalBuilds} builds across projects, avg composite: ${avgComposite.toFixed(2)}`;
459
+ lines.push(line1);
460
+ chars += line1.length;
461
+
462
+ // Weakest stage globally
463
+ const attrAvgs = {};
464
+ for (const key of Object.keys(attrSums)) {
465
+ if (attrCounts[key] > 0) attrAvgs[key] = attrSums[key] / attrCounts[key];
466
+ }
467
+ const stages = Object.entries(attrAvgs).sort((a, b) => a[1] - b[1]);
468
+ if (stages.length > 0 && stages[0][1] < 0.5) {
469
+ const line = `Global weak stage: ${stages[0][0]} (avg ${stages[0][1].toFixed(2)}) — consistently underperforms across projects.`;
470
+ if (chars + line.length <= MAX_GLOBAL_CALIBRATION_CHARS) { lines.push(line); chars += line.length; }
471
+ }
472
+
473
+ // Recurring quality issues
474
+ const recurring = Object.entries(allFindings).filter(([, c]) => c >= 2).sort((a, b) => b[1] - a[1]).slice(0, 3);
475
+ if (recurring.length > 0) {
476
+ const line = `Recurring quality issues: ${recurring.map(([f, c]) => `${f} (${c}x)`).join(", ")}`;
477
+ if (chars + line.length <= MAX_GLOBAL_CALIBRATION_CHARS) { lines.push(line); chars += line.length; }
478
+ }
479
+
480
+ // Recurring sentinel concerns
481
+ const recurringConcerns = Object.entries(allConcerns).filter(([, c]) => c >= 2).sort((a, b) => b[1] - a[1]).slice(0, 2);
482
+ if (recurringConcerns.length > 0) {
483
+ const line = `Recurring sentinel flags: ${recurringConcerns.map(([c, n]) => `${c.slice(0, 60)} (${n}x)`).join("; ")}`;
484
+ if (chars + line.length <= MAX_GLOBAL_CALIBRATION_CHARS) { lines.push(line); chars += line.length; }
485
+ }
486
+
487
+ return lines.join("\n");
488
+ }
489
+
372
490
  // --- Layer 2: Context injection ---
373
491
 
374
492
  export function buildCalibrationBlock(scorecards) {
package/src/voice.js CHANGED
@@ -714,14 +714,20 @@ export async function setupVoice(onStatus) {
714
714
  playArgs = [mp3Path];
715
715
  } else if (process.platform === "win32") {
716
716
  // Use sox to play the audio — it's already installed for recording
717
- if (soxPath) {
718
- playCmd = soxPath;
719
- playArgs = [mp3Path, "-d"];
720
- } else {
721
- // Fallback: PowerShell Start-Process to open in default player
722
- playCmd = "powershell.exe";
723
- playArgs = ["-NoProfile", "-Command", `Start-Process "${mp3Path}" -Wait`];
724
- }
717
+ // Use PowerShell to play MP3 via Windows Media Player COM
718
+ const psScript = `
719
+ $wmp = New-Object -ComObject WMPlayer.OCX
720
+ $wmp.URL = "${mp3Path.replace(/\\/g, "\\\\")}"
721
+ $wmp.controls.play()
722
+ Start-Sleep -Milliseconds 500
723
+ while($wmp.playState -eq 3){ Start-Sleep -Milliseconds 200 }
724
+ $wmp.close()
725
+ `.trim();
726
+ const psPath = path.join(tmpDir, `tts-${ts}.ps1`);
727
+ await fs.writeFile(psPath, psScript);
728
+ cleanupFiles.push(psPath);
729
+ playCmd = "powershell.exe";
730
+ playArgs = ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", psPath];
725
731
  } else {
726
732
  playCmd = "mpv";
727
733
  playArgs = ["--no-video", "--really-quiet", mp3Path];
package/src/workflow.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  } from "./frontend.js";
17
17
  import { runPlannerPass, formatPlanForExecutor, formatPlanForDisplay } from "./planner.js";
18
18
  import { runVerificationPass, formatVerifierResults, hasFailures } from "./verifier.js";
19
- import { computeScorecard, saveScorecard, findRelevantScorecards, buildCalibrationBlock, generatePredictions } from "./scorecard.js";
19
+ import { computeScorecard, saveScorecard, findRelevantScorecards, buildCalibrationBlock, generatePredictions, loadGlobalScorecards, buildGlobalCalibrationBlock } from "./scorecard.js";
20
20
 
21
21
  export async function runBuildWorkflow({
22
22
  agent,
@@ -40,6 +40,12 @@ export async function runBuildWorkflow({
40
40
  predictions = generatePredictions(relevantCards);
41
41
  }
42
42
  }
43
+ // Global calibration — cross-project learning
44
+ const globalCards = await loadGlobalScorecards({ limit: 20 });
45
+ const globalBlock = buildGlobalCalibrationBlock(globalCards);
46
+ if (globalBlock) {
47
+ calibrationBlock = calibrationBlock ? `${calibrationBlock}\n\n${globalBlock}` : globalBlock;
48
+ }
43
49
  } catch {}
44
50
 
45
51
  // Planner/Executor split: if plannerModel is configured, run planner first