@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 +1 -1
- package/src/scorecard.js +120 -2
- package/src/voice.js +14 -8
- package/src/workflow.js +7 -1
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|