@tritard/waterbrother 0.16.108 → 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 +155 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.108",
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;
@@ -862,7 +892,9 @@ function parseTelegramAgentIntent(text = "") {
862
892
  /^(?:have|make|set)\s+(.+?)['’]s\s+(?:bot|terminal)\s+(?:be|as|to)?\s*(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i,
863
893
  /^(?:have|make|set)\s+(.+?)\s+(?:bot|terminal)\s+(?:be|as|to)?\s*(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i,
864
894
  /^(.+?)['’]s\s+(?:bot|terminal)\s+should\s+(?:be\s+)?(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i,
865
- /^(.+?)\s+(?:bot|terminal)\s+should\s+(?:be\s+)?(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i
895
+ /^(.+?)\s+(?:bot|terminal)\s+should\s+(?:be\s+)?(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i,
896
+ /^(?:have|make|set)\s+(.+?)\s+(?:be|as|to)?\s*(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i,
897
+ /^(.+?)\s+should\s+(?:be\s+)?(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i
866
898
  ];
867
899
  for (const pattern of patterns) {
868
900
  const match = value.match(pattern);
@@ -914,6 +946,12 @@ function parseTelegramStateIntent(text = "") {
914
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)) {
915
947
  return { action: /\bshow latest verification result\b/.test(lower) || /\bdid verification pass\b/.test(lower) ? "verification-status" : "run-verification" };
916
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
+ }
917
955
  if (/\bverification mode\b/.test(lower)) {
918
956
  const modeMatch = lower.match(/\b(off|manual|auto|blocking)\b/);
919
957
  return modeMatch?.[1] ? { action: "verification-mode-set", mode: modeMatch[1] } : { action: "verification-mode-status" };
@@ -2433,6 +2471,29 @@ class TelegramGateway {
2433
2471
  }
2434
2472
  };
2435
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
+ }
2436
2497
  return null;
2437
2498
  }
2438
2499
 
@@ -2530,6 +2591,37 @@ class TelegramGateway {
2530
2591
  return formatVerificationResultMarkup(result, verifier);
2531
2592
  }
2532
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
+
2533
2625
  if (intent.action === "run-verification") {
2534
2626
  if (!project?.enabled) {
2535
2627
  return "This project is not shared.";
@@ -3544,7 +3636,10 @@ class TelegramGateway {
3544
3636
  const sourcePrefix = sourceHost
3545
3637
  ? `<b>Handled by</b>\n<code>${escapeTelegramHtml(formatBridgeHostLabel(sourceHost) || sourceHost.sessionId || "live terminal")}</code>\n\n`
3546
3638
  : "";
3547
- const chunks = renderTelegramChunks(`${sourcePrefix}${String(content || "")}`);
3639
+ const contentChunks = renderTelegramChunks(String(content || ""));
3640
+ const chunks = contentChunks.length
3641
+ ? [sourcePrefix ? `${sourcePrefix}${contentChunks[0]}` : contentChunks[0], ...contentChunks.slice(1)]
3642
+ : (sourcePrefix ? [sourcePrefix.trim()] : []);
3548
3643
  if (!chunks.length) {
3549
3644
  if (previewMessage?.message_id) {
3550
3645
  await this.editMessage(chatId, previewMessage.message_id, "(no content)");
@@ -4618,6 +4713,31 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
4618
4713
  });
4619
4714
  return;
4620
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
+ }
4621
4741
  const liveHosts = await this.getLiveBridgeHosts({ cwd: operatorGate.session?.cwd || this.cwd });
4622
4742
  const host = await this.getLiveBridgeHost();
4623
4743
  const activeExecutor = {
@@ -4684,6 +4804,39 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
4684
4804
  sourceHost: typeof result === "string" ? null : (result?.sourceHost || null)
4685
4805
  });
4686
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
+ }
4687
4840
  } catch (error) {
4688
4841
  await this.deliverPromptFailure(message.chat.id, message.message_id, previewMessage, error);
4689
4842
  } finally {