@tritard/waterbrother 0.16.109 → 0.16.110

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/gateway.js +148 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.109",
3
+ "version": "0.16.110",
4
4
  "description": "Waterbrother: bring-your-own-model coding CLI with local tools, sessions, operator modes, and approval controls",
5
5
  "type": "module",
6
6
  "bin": {
package/src/gateway.js CHANGED
@@ -646,6 +646,36 @@ function getLatestReviewResult(project) {
646
646
  return ordered.find((event) => String(event?.type || "").trim() === "review-result") || null;
647
647
  }
648
648
 
649
+ function getBlockingVerificationResult(project) {
650
+ if (String(project?.verificationMode || "manual").trim() !== "blocking") {
651
+ return null;
652
+ }
653
+ const latestResult = getLatestVerificationResult(project);
654
+ const latestId = String(latestResult?.id || "").trim();
655
+ const latestOutcome = String(latestResult?.outcome || "").trim().toLowerCase();
656
+ if (!latestId || !latestOutcome || latestOutcome === "passed") {
657
+ return null;
658
+ }
659
+ const events = Array.isArray(project?.recentEvents) ? [...project.recentEvents] : [];
660
+ const ordered = events
661
+ .filter((event) => event?.createdAt)
662
+ .sort((a, b) => String(b.createdAt || "").localeCompare(String(a.createdAt || "")));
663
+ for (const event of ordered) {
664
+ const type = String(event?.type || "").trim();
665
+ const meta = event?.meta && typeof event.meta === "object" ? event.meta : {};
666
+ if (String(meta.verificationResultId || "").trim() !== latestId) {
667
+ continue;
668
+ }
669
+ if (type === "verification-override") {
670
+ return null;
671
+ }
672
+ if (type === "verification-result") {
673
+ return latestResult;
674
+ }
675
+ }
676
+ return latestResult;
677
+ }
678
+
649
679
  function memberRoleWeight(role = "") {
650
680
  const normalized = String(role || "").trim().toLowerCase();
651
681
  if (normalized === "owner") return 3;
@@ -916,6 +946,12 @@ function parseTelegramStateIntent(text = "") {
916
946
  if (/^(?:run|start|do)\s+verification\b/.test(lower) || /^(?:verify)\b/.test(lower) || /\bshow latest verification result\b/.test(lower) || /\bdid verification pass\b/.test(lower)) {
917
947
  return { action: /\bshow latest verification result\b/.test(lower) || /\bdid verification pass\b/.test(lower) ? "verification-status" : "run-verification" };
918
948
  }
949
+ if (/\boverride verification\b/.test(lower)) {
950
+ return { action: "verification-override" };
951
+ }
952
+ if (/\bre-?run verification\b/.test(lower) || /\brun verification again\b/.test(lower) || /\bverify again\b/.test(lower)) {
953
+ return { action: "verification-rerun" };
954
+ }
919
955
  if (/\bverification mode\b/.test(lower)) {
920
956
  const modeMatch = lower.match(/\b(off|manual|auto|blocking)\b/);
921
957
  return modeMatch?.[1] ? { action: "verification-mode-set", mode: modeMatch[1] } : { action: "verification-mode-status" };
@@ -2435,6 +2471,29 @@ class TelegramGateway {
2435
2471
  }
2436
2472
  };
2437
2473
  }
2474
+ if (continuation.kind === "blocking-verification-override") {
2475
+ const reply = this.stripBotMention(String(message?.text || "").trim()).toLowerCase();
2476
+ if (/\boverride verification\b/.test(reply) || /\boverride\b/.test(reply)) {
2477
+ return {
2478
+ markup: await this.handleStateIntent(message, sessionId, { action: "verification-override" })
2479
+ };
2480
+ }
2481
+ if (/\bre-?run verification\b/.test(reply) || /\brerun\b/.test(reply) || /\bverify again\b/.test(reply)) {
2482
+ return {
2483
+ markup: await this.handleStateIntent(message, sessionId, { action: "verification-rerun" })
2484
+ };
2485
+ }
2486
+ return {
2487
+ markup: "Reply with <code>rerun verification</code> or <code>override verification</code>.",
2488
+ remember: {
2489
+ text: String(continuation.lastPrompt || "").trim() || "Reply with rerun verification or override verification.",
2490
+ kind: continuation.kind,
2491
+ source: continuation.source,
2492
+ context: continuation.context || {},
2493
+ force: true
2494
+ }
2495
+ };
2496
+ }
2438
2497
  return null;
2439
2498
  }
2440
2499
 
@@ -2532,6 +2591,37 @@ class TelegramGateway {
2532
2591
  return formatVerificationResultMarkup(result, verifier);
2533
2592
  }
2534
2593
 
2594
+ if (intent.action === "verification-override") {
2595
+ if (!project?.enabled) {
2596
+ return "This project is not shared.";
2597
+ }
2598
+ const blockingVerification = getBlockingVerificationResult(project);
2599
+ if (!blockingVerification) {
2600
+ return "<b>Verification override</b>\nNo blocking verification is active.";
2601
+ }
2602
+ await addSharedRoomNote(cwd, "Blocking verification overridden for now", {
2603
+ actorId,
2604
+ actorName,
2605
+ type: "verification-override",
2606
+ meta: {
2607
+ verificationResultId: String(blockingVerification.id || "").trim(),
2608
+ outcome: String(blockingVerification.outcome || "").trim()
2609
+ }
2610
+ });
2611
+ return [
2612
+ "<b>Verification overridden</b>",
2613
+ `latest outcome: <code>${escapeTelegramHtml(String(blockingVerification.outcome || "failed"))}</code>`,
2614
+ "Execution can proceed for now."
2615
+ ].join("\n");
2616
+ }
2617
+
2618
+ if (intent.action === "verification-rerun") {
2619
+ if (!project?.enabled) {
2620
+ return "This project is not shared.";
2621
+ }
2622
+ return this.handleStateIntent(message, sessionId, { action: "run-verification" });
2623
+ }
2624
+
2535
2625
  if (intent.action === "run-verification") {
2536
2626
  if (!project?.enabled) {
2537
2627
  return "This project is not shared.";
@@ -4623,6 +4713,31 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
4623
4713
  });
4624
4714
  return;
4625
4715
  }
4716
+ const blockingVerification = getBlockingVerificationResult(operatorGate.project);
4717
+ if (blockingVerification) {
4718
+ const followUp = "Reply with rerun verification to try again, or override verification to proceed anyway.";
4719
+ await this.sendMarkup(
4720
+ message.chat.id,
4721
+ [
4722
+ "<b>Blocking verification in effect</b>",
4723
+ `latest outcome: <code>${escapeTelegramHtml(String(blockingVerification.outcome || "failed"))}</code>`,
4724
+ blockingVerification.summary ? `summary: <code>${escapeTelegramHtml(String(blockingVerification.summary || ""))}</code>` : "",
4725
+ "Execution is paused until verification passes or the room overrides it.",
4726
+ followUp
4727
+ ].filter(Boolean).join("\n"),
4728
+ message.message_id
4729
+ );
4730
+ await this.rememberContinuationWithOptions(message, followUp, {
4731
+ kind: "blocking-verification-override",
4732
+ source: "verification-policy-gate",
4733
+ context: {
4734
+ verificationResultId: String(blockingVerification.id || "").trim(),
4735
+ outcome: String(blockingVerification.outcome || "").trim()
4736
+ },
4737
+ force: true
4738
+ });
4739
+ return;
4740
+ }
4626
4741
  const liveHosts = await this.getLiveBridgeHosts({ cwd: operatorGate.session?.cwd || this.cwd });
4627
4742
  const host = await this.getLiveBridgeHost();
4628
4743
  const activeExecutor = {
@@ -4689,6 +4804,39 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
4689
4804
  sourceHost: typeof result === "string" ? null : (result?.sourceHost || null)
4690
4805
  });
4691
4806
  await this.rememberContinuation(message, finalContent);
4807
+ if (["auto", "blocking"].includes(String(operatorGate.project?.verificationMode || "manual").trim())) {
4808
+ const verificationMarkup = await this.handleStateIntent(message, sessionId, { action: "run-verification" });
4809
+ if (verificationMarkup) {
4810
+ await this.sendMarkup(message.chat.id, verificationMarkup, message.message_id);
4811
+ }
4812
+ if (String(operatorGate.project?.verificationMode || "manual").trim() === "blocking") {
4813
+ const nextProject = await loadSharedProject(operatorGate.session?.cwd || this.cwd);
4814
+ const nextBlockingVerification = getBlockingVerificationResult(nextProject);
4815
+ if (nextBlockingVerification) {
4816
+ const followUp = "Reply with rerun verification to try again, or override verification to proceed anyway.";
4817
+ await this.sendMarkup(
4818
+ message.chat.id,
4819
+ [
4820
+ "<b>Blocking verification in effect</b>",
4821
+ `latest outcome: <code>${escapeTelegramHtml(String(nextBlockingVerification.outcome || "failed"))}</code>`,
4822
+ nextBlockingVerification.summary ? `summary: <code>${escapeTelegramHtml(String(nextBlockingVerification.summary || ""))}</code>` : "",
4823
+ "Further execution is paused until verification passes or the room overrides it.",
4824
+ followUp
4825
+ ].filter(Boolean).join("\n"),
4826
+ message.message_id
4827
+ );
4828
+ await this.rememberContinuationWithOptions(message, followUp, {
4829
+ kind: "blocking-verification-override",
4830
+ source: "verification-policy-gate",
4831
+ context: {
4832
+ verificationResultId: String(nextBlockingVerification.id || "").trim(),
4833
+ outcome: String(nextBlockingVerification.outcome || "").trim()
4834
+ },
4835
+ force: true
4836
+ });
4837
+ }
4838
+ }
4839
+ }
4692
4840
  } catch (error) {
4693
4841
  await this.deliverPromptFailure(message.chat.id, message.message_id, previewMessage, error);
4694
4842
  } finally {