@tritard/waterbrother 0.16.97 → 0.16.99

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.16.97",
3
+ "version": "0.16.99",
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
@@ -9,7 +9,7 @@ import { DEFAULT_PENDING_PAIRING_TTL_MINUTES, loadGatewayBridge, loadGatewayStat
9
9
  import { getGatewayStatus, getChannelSpec } from "./channels.js";
10
10
  import { getConfigPath, loadConfigLayers, saveConfig } from "./config.js";
11
11
  import { canonicalizeLoosePath } from "./path-utils.js";
12
- import { acceptSharedInvite, addSharedRoomNote, addSharedTask, approveSharedInvite, assignSharedTask, claimSharedOperator, claimSharedTask, commentSharedTask, createSharedInvite, enableSharedProject, getSharedTaskHistory, listSharedEvents, listSharedInvites, listSharedTasks, loadSharedProject, moveSharedTask, rejectSharedInvite, releaseSharedOperator, setSharedRoom, setSharedRoomMode, setSharedRuntimeProfile, removeSharedMember, upsertSharedAgent, upsertSharedMember } from "./shared-project.js";
12
+ import { acceptSharedInvite, addSharedRoomNote, addSharedTask, addVerificationResult, approveSharedInvite, assignSharedTask, claimSharedOperator, claimSharedTask, commentSharedTask, createSharedInvite, enableSharedProject, getLatestVerificationResult, getSharedTaskHistory, listSharedEvents, listSharedInvites, listSharedTasks, loadSharedProject, moveSharedTask, rejectSharedInvite, releaseSharedOperator, removeSharedMember, setSharedRoom, setSharedRoomMode, setSharedRuntimeProfile, setSharedVerificationMode, upsertSharedAgent, upsertSharedMember } from "./shared-project.js";
13
13
  import { buildSelfAwarenessManifest, formatAboutWaterbrother, formatSelfState, resolveLocalConceptQuestion } from "./self-awareness.js";
14
14
 
15
15
  const execFileAsync = promisify(execFile);
@@ -296,6 +296,7 @@ function buildTelegramRoomOnboardingMarkup({ project = null, actorName = "", exe
296
296
  const participants = listProjectParticipants(project);
297
297
  const agents = listProjectAgents(project);
298
298
  const reviewer = chooseReviewerAgent(project);
299
+ const verifier = chooseVerifierAgent(project);
299
300
  const blockingReview = getLatestBlockingReviewPolicy(project);
300
301
  return [
301
302
  "<b>Roundtable room</b>",
@@ -305,6 +306,7 @@ function buildTelegramRoomOnboardingMarkup({ project = null, actorName = "", exe
305
306
  `agents: <code>${escapeTelegramHtml(String(agents.length || 0))}</code>`,
306
307
  executor?.provider && executor?.model ? `active runtime: <code>${escapeTelegramHtml(`${executor.provider}/${executor.model}`)}</code>` : "",
307
308
  reviewer ? `reviewer: <code>${escapeTelegramHtml(reviewer.ownerName || reviewer.label || reviewer.ownerId || reviewer.id || "unknown")}</code>` : "",
309
+ verifier ? `verifier: <code>${escapeTelegramHtml(verifier.ownerName || verifier.label || verifier.ownerId || verifier.id || "unknown")}</code>` : "",
308
310
  `blocking review: <code>${escapeTelegramHtml(blockingReview ? "yes" : "no")}</code>`,
309
311
  "",
310
312
  "<b>What you can do here</b>",
@@ -312,6 +314,7 @@ function buildTelegramRoomOnboardingMarkup({ project = null, actorName = "", exe
312
314
  "• ask <code>who is in the room?</code>",
313
315
  "• say <code>add Austin as editor</code>",
314
316
  "• ask <code>which terminals are live?</code>",
317
+ "• say <code>run verification</code>",
315
318
  "• in execute mode, address Waterbrother directly to build or change code",
316
319
  "",
317
320
  "Use <code>/help</code> for the full command list."
@@ -336,7 +339,9 @@ function buildTelegramRoomGuidanceMarkup({ project = null, member = null, execut
336
339
  "• <code>what project is this chat bound to?</code>",
337
340
  "• <code>who is in the room?</code>",
338
341
  "• <code>who are the bots?</code>",
339
- role === "owner" ? "• <code>add Austin as editor</code>" : "• <code>what mode are we in?</code>"
342
+ role === "owner" ? "• <code>add Austin as editor</code>" : "• <code>what mode are we in?</code>",
343
+ "• <code>who is the verifier?</code>",
344
+ "• <code>run verification</code>"
340
345
  ].filter(Boolean).join("\n");
341
346
  }
342
347
 
@@ -353,6 +358,8 @@ function buildTelegramAgentAnnouncementMarkup(event = {}) {
353
358
  ? "This terminal is the selected executor and can take room work when it is live."
354
359
  : role === "reviewer"
355
360
  ? "This terminal is the room reviewer and can be asked for a second opinion or blocking review."
361
+ : role === "verifier"
362
+ ? "This terminal is the verifier and can run tests, checks, and build verification for the room."
356
363
  : "This terminal is on standby until the room assigns it a more active role.";
357
364
  return [
358
365
  `<b>${escapeTelegramHtml(title)}</b>`,
@@ -363,7 +370,7 @@ function buildTelegramAgentAnnouncementMarkup(event = {}) {
363
370
  runtime ? `runtime: <code>${escapeTelegramHtml(runtime)}</code>` : "",
364
371
  meta.runtimeProfile ? `runtime profile: <code>${escapeTelegramHtml(String(meta.runtimeProfile || ""))}</code>` : "",
365
372
  roleGuidance,
366
- "Examples: <code>which terminals are live?</code>, <code>make this terminal the reviewer</code>, <code>make this terminal the executor</code>"
373
+ "Examples: <code>which terminals are live?</code>, <code>make this terminal the reviewer</code>, <code>make this terminal the verifier</code>, <code>make this terminal the executor</code>"
367
374
  ].filter(Boolean).join("\n");
368
375
  }
369
376
 
@@ -502,6 +509,32 @@ function summarizeRuntimeConflict(project, fallbackExecutor = {}) {
502
509
  };
503
510
  }
504
511
 
512
+ function cleanAgentDisplayValue(value = "") {
513
+ const cleaned = String(value || "").replace(/\bundefined\b/ig, "").trim();
514
+ if (!cleaned) return "";
515
+ if (/^terminal$/i.test(cleaned)) return "";
516
+ return cleaned;
517
+ }
518
+
519
+ function getAgentOwnerDisplay(agent = {}, fallback = "") {
520
+ const ownerName = cleanAgentDisplayValue(agent?.ownerName);
521
+ const ownerId = cleanAgentDisplayValue(agent?.ownerId);
522
+ const label = cleanAgentDisplayValue(agent?.label);
523
+ const fallbackValue = cleanAgentDisplayValue(fallback);
524
+ return ownerName || ownerId || fallbackValue || label || "unknown";
525
+ }
526
+
527
+ function getAgentTerminalDisplay(agent = {}, fallback = "") {
528
+ const label = cleanAgentDisplayValue(agent?.label);
529
+ const fallbackValue = cleanAgentDisplayValue(fallback);
530
+ if (label) return label;
531
+ if (fallbackValue) return fallbackValue;
532
+ const ownerName = cleanAgentDisplayValue(agent?.ownerName);
533
+ if (ownerName) return ownerName + " terminal";
534
+ const ownerId = cleanAgentDisplayValue(agent?.ownerId);
535
+ if (ownerId) return ownerId + " terminal";
536
+ return "current terminal";
537
+ }
505
538
  function formatBridgeHostLabel(host = {}) {
506
539
  const owner = String(host?.ownerName || host?.ownerId || "").trim();
507
540
  const label = String(host?.label || "").trim();
@@ -526,6 +559,11 @@ function chooseReviewerAgent(project) {
526
559
  return agents.find((agent) => String(agent?.role || "").trim() === "reviewer") || null;
527
560
  }
528
561
 
562
+ function chooseVerifierAgent(project) {
563
+ const agents = listProjectAgents(project);
564
+ return agents.find((agent) => String(agent?.role || "").trim() === "verifier") || null;
565
+ }
566
+
529
567
  function summarizeExecutorReviewerArbitration(project, fallbackExecutor = {}) {
530
568
  const executorAgent = chooseExecutorAgent(project, fallbackExecutor);
531
569
  const reviewerAgent = chooseReviewerAgent(project);
@@ -541,6 +579,29 @@ function summarizeExecutorReviewerArbitration(project, fallbackExecutor = {}) {
541
579
  };
542
580
  }
543
581
 
582
+ function formatVerificationResultMarkup(result = {}, verifier = null) {
583
+ const lines = [
584
+ "<b>Verification result</b>",
585
+ `outcome: <code>${escapeTelegramHtml(String(result.outcome || "failed"))}</code>`
586
+ ];
587
+ if (verifier) {
588
+ lines.splice(1, 0, `verifier: <code>${escapeTelegramHtml(verifier.ownerName || verifier.label || verifier.ownerId || verifier.id || "unknown")}</code>`);
589
+ }
590
+ if (Array.isArray(result.commands) && result.commands.length) {
591
+ lines.push("commands:");
592
+ for (const command of result.commands) {
593
+ lines.push(`• <code>${escapeTelegramHtml(command)}</code>`);
594
+ }
595
+ }
596
+ if (result.summary) {
597
+ lines.push("", "<b>Summary</b>", escapeTelegramHtml(result.summary));
598
+ }
599
+ if (Array.isArray(result.logs) && result.logs.length) {
600
+ lines.push("", "<b>Top errors</b>", ...result.logs.slice(0, 6).map((line) => `• ${escapeTelegramHtml(line)}`));
601
+ }
602
+ return lines.filter(Boolean).join("\n");
603
+ }
604
+
544
605
  function parseReviewerOutcome(text = "") {
545
606
  const value = String(text || "").trim();
546
607
  const lower = value.toLowerCase();
@@ -660,7 +721,11 @@ function formatTelegramProjectMarkup({ cwd, project, chatId = "", title = "" })
660
721
  }
661
722
 
662
723
  function normalizeTelegramProjectIntentText(text = "") {
663
- return String(text || "").trim().replace(/\s+/g, " ");
724
+ return String(text || "")
725
+ .replace(/[‘’′]/g, "'")
726
+ .replace(/[“”]/g, '"')
727
+ .trim()
728
+ .replace(/\s+/g, " ");
664
729
  }
665
730
 
666
731
  function parseTelegramProjectIntent(text = "") {
@@ -783,15 +848,15 @@ function parseTelegramAgentIntent(text = "") {
783
848
  }
784
849
  }
785
850
 
786
- const roleMatch = lowered.match(/\b(executor|reviewer|standby)\b/);
851
+ const roleMatch = lowered.match(/\b(executor|reviewer|verifier|standby)\b/);
787
852
  const role = roleMatch?.[1] || "";
788
853
  if (!role) return null;
789
854
 
790
855
  const patterns = [
791
- /^(?:have|make|set)\s+(.+?)['’]s\s+(?:bot|terminal)\s+(?:be|as|to)?\s*(?:the\s+)?(executor|reviewer|standby)\s*$/i,
792
- /^(?:have|make|set)\s+(.+?)\s+(?:bot|terminal)\s+(?:be|as|to)?\s*(?:the\s+)?(executor|reviewer|standby)\s*$/i,
793
- /^(.+?)['’]s\s+(?:bot|terminal)\s+should\s+(?:be\s+)?(?:the\s+)?(executor|reviewer|standby)\s*$/i,
794
- /^(.+?)\s+(?:bot|terminal)\s+should\s+(?:be\s+)?(?:the\s+)?(executor|reviewer|standby)\s*$/i
856
+ /^(?:have|make|set)\s+(.+?)['’]s\s+(?:bot|terminal)\s+(?:be|as|to)?\s*(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i,
857
+ /^(?:have|make|set)\s+(.+?)\s+(?:bot|terminal)\s+(?:be|as|to)?\s*(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i,
858
+ /^(.+?)['’]s\s+(?:bot|terminal)\s+should\s+(?:be\s+)?(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i,
859
+ /^(.+?)\s+(?:bot|terminal)\s+should\s+(?:be\s+)?(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i
795
860
  ];
796
861
  for (const pattern of patterns) {
797
862
  const match = value.match(pattern);
@@ -834,9 +899,28 @@ function parseTelegramStateIntent(text = "") {
834
899
  if (/\bwhat can i do here\b/.test(lower) || /\bhow do i use this room\b/.test(lower) || /\bhow do i use this chat\b/.test(lower) || /\bwhat do i do here\b/.test(lower)) {
835
900
  return { action: "room-guidance" };
836
901
  }
902
+ if (/\bwho(?:'s| is)? the verifier\b/.test(lower) || /\bwhat is the verifier\b/.test(lower) || /\bwho should verify this\b/.test(lower)) {
903
+ return { action: "verifier-status" };
904
+ }
837
905
  if (/\bwho are the (?:bots|boys)\b/.test(lower) || /\bwho are the agents\b/.test(lower) || /\bwhat (?:bots|boys) are here\b/.test(lower) || /\bwhat agents are here\b/.test(lower)) {
838
906
  return { action: "agent-list" };
839
907
  }
908
+ 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)) {
909
+ return { action: /\bshow latest verification result\b/.test(lower) || /\bdid verification pass\b/.test(lower) ? "verification-status" : "run-verification" };
910
+ }
911
+ if (/\bverification mode\b/.test(lower)) {
912
+ const modeMatch = lower.match(/\b(off|manual|auto|blocking)\b/);
913
+ return modeMatch?.[1] ? { action: "verification-mode-set", mode: modeMatch[1] } : { action: "verification-mode-status" };
914
+ }
915
+ if (/\balways verify after execution\b/.test(lower)) {
916
+ return { action: "verification-mode-set", mode: "auto" };
917
+ }
918
+ if (/\bturn off verification\b/.test(lower)) {
919
+ return { action: "verification-mode-set", mode: "off" };
920
+ }
921
+ if (/\bverification should be blocking\b/.test(lower)) {
922
+ return { action: "verification-mode-set", mode: "blocking" };
923
+ }
840
924
  if (/\bwhich terminals are live\b/.test(lower) || /\bwhich bots are live\b/.test(lower) || /\bwho is live\b/.test(lower) || /\bwhat terminals are live\b/.test(lower)) {
841
925
  return { action: "live-hosts" };
842
926
  }
@@ -980,11 +1064,14 @@ function formatTelegramRoomMarkup(project, options = {}) {
980
1064
  const liveHosts = Array.isArray(options.liveHosts) ? options.liveHosts : [];
981
1065
  const selectedExecutor = chooseExecutorAgent(project, executor);
982
1066
  const selectedReviewer = chooseReviewerAgent(project);
1067
+ const selectedVerifier = chooseVerifierAgent(project);
983
1068
  const blockingReview = getLatestBlockingReviewPolicy(project);
984
1069
  const latestReviewResult = getLatestReviewResult(project);
985
1070
  const latestReviewMeta = latestReviewResult?.meta && typeof latestReviewResult.meta === "object" ? latestReviewResult.meta : {};
1071
+ const latestVerificationResult = getLatestVerificationResult(project);
986
1072
  const selectedExecutorLiveHost = selectedExecutor ? findLiveHostForAgent(liveHosts, selectedExecutor) : null;
987
1073
  const selectedReviewerLiveHost = selectedReviewer ? findLiveHostForAgent(liveHosts, selectedReviewer) : null;
1074
+ const selectedVerifierLiveHost = selectedVerifier ? findLiveHostForAgent(liveHosts, selectedVerifier) : null;
988
1075
  const executorBits = [
989
1076
  `surface: <code>${escapeTelegramHtml(executor.surface || "telegram")}</code>`,
990
1077
  `provider: <code>${escapeTelegramHtml(executor.provider || "unknown")}</code>`,
@@ -1012,8 +1099,12 @@ function formatTelegramRoomMarkup(project, options = {}) {
1012
1099
  `executor live: <code>${escapeTelegramHtml(selectedExecutorLiveHost ? "yes" : "no")}</code>`,
1013
1100
  `reviewer: <code>${escapeTelegramHtml(selectedReviewer ? (selectedReviewer.ownerName || selectedReviewer.label || selectedReviewer.ownerId || selectedReviewer.id || "none") : "none")}</code>`,
1014
1101
  `reviewer live: <code>${escapeTelegramHtml(selectedReviewerLiveHost ? "yes" : "no")}</code>`,
1102
+ `verifier: <code>${escapeTelegramHtml(selectedVerifier ? (selectedVerifier.ownerName || selectedVerifier.label || selectedVerifier.ownerId || selectedVerifier.id || "none") : "none")}</code>`,
1103
+ `verifier live: <code>${escapeTelegramHtml(selectedVerifierLiveHost ? "yes" : "no")}</code>`,
1015
1104
  `blocking review: <code>${escapeTelegramHtml(blockingReview ? "yes" : "no")}</code>`,
1016
1105
  `latest reviewer outcome: <code>${escapeTelegramHtml(String(latestReviewMeta.outcome || "").trim() || "none")}</code>`,
1106
+ `verification mode: <code>${escapeTelegramHtml(String(project.verificationMode || "manual"))}</code>`,
1107
+ `latest verification outcome: <code>${escapeTelegramHtml(String(latestVerificationResult?.outcome || "").trim() || "none")}</code>`,
1017
1108
  "<b>Executor</b>",
1018
1109
  ...executorBits,
1019
1110
  "<b>Runtime Split</b>",
@@ -1946,7 +2037,31 @@ class TelegramGateway {
1946
2037
  throw new Error("Only a shared-project owner can change terminal roles.");
1947
2038
  }
1948
2039
  const actor = this.describeTelegramUser(message?.from || {});
1949
- const targetAgent = resolveProjectAgent(project, intent.target);
2040
+ const normalizedTarget = String(intent.target || "").trim().toLowerCase();
2041
+ let targetAgent = resolveProjectAgent(project, intent.target);
2042
+ if (!targetAgent && /^(?:this|my)(?:\s+(?:bot|terminal))?$/.test(normalizedTarget)) {
2043
+ const host = await this.getLiveBridgeHost();
2044
+ if (host) {
2045
+ targetAgent = listProjectAgents(project).find((agent) => (
2046
+ String(agent?.sessionId || "").trim() && String(agent.sessionId || "").trim() === String(host.sessionId || "").trim()
2047
+ ) || (
2048
+ String(agent?.ownerId || "").trim() && String(agent.ownerId || "").trim() === String(host.ownerId || "").trim()
2049
+ )) || {
2050
+ id: `agent:telegram-bridge:${String(host.sessionId || host.pid || "current").trim()}`,
2051
+ ownerId: String(host.ownerId || actor.userId || "").trim(),
2052
+ ownerName: getAgentOwnerDisplay(host, actor.displayName || actor.userId || "current operator"),
2053
+ label: getAgentTerminalDisplay(host, actor.displayName ? actor.displayName + " terminal" : (actor.userId ? actor.userId + " terminal" : "current terminal")),
2054
+ surface: String(host.surface || "live-tui").trim(),
2055
+ role: "standby",
2056
+ provider: String(host.provider || "").trim(),
2057
+ model: String(host.model || "").trim(),
2058
+ runtimeProfile: String(host.runtimeProfile || "").trim(),
2059
+ sessionId: String(host.sessionId || "").trim(),
2060
+ cwd: String(session.cwd || this.cwd).trim(),
2061
+ chatId: String(message?.chat?.id || "").trim()
2062
+ };
2063
+ }
2064
+ }
1950
2065
  if (!targetAgent) {
1951
2066
  throw new Error(`No terminal found for ${intent.target}. Ask them to connect their Waterbrother bot/terminal first.`);
1952
2067
  }
@@ -1958,26 +2073,30 @@ class TelegramGateway {
1958
2073
  actorName: actor.displayName || actorId
1959
2074
  });
1960
2075
  const runtimeLabel = targetAgent.provider && targetAgent.model ? `${targetAgent.provider}/${targetAgent.model}` : "unknown";
1961
- const actionTitle = intent.action === "agent-review"
2076
+ const actionTitle = intent.action === "agent-review" || intent.role === "reviewer"
1962
2077
  ? "Roundtable reviewer assigned"
1963
- : intent.action === "agent-execute"
2078
+ : intent.action === "agent-execute" || intent.role === "executor"
1964
2079
  ? "Roundtable executor assigned"
1965
- : "Roundtable terminal updated";
1966
- const note = intent.action === "agent-review"
2080
+ : intent.role === "verifier"
2081
+ ? "Roundtable verifier assigned"
2082
+ : "Roundtable terminal updated";
2083
+ const note = intent.action === "agent-review" || intent.role === "reviewer"
1967
2084
  ? "This sets the room reviewer role. It does not automatically run a review pass yet."
1968
- : intent.action === "agent-execute"
2085
+ : intent.action === "agent-execute" || intent.role === "executor"
1969
2086
  ? "This sets the room executor role. Claim/mode rules still control actual execution."
1970
- : "";
1971
- const followUp = intent.action === "agent-review"
1972
- ? `Should ${targetAgent.ownerName || targetAgent.label || "that terminal"} review be advisory or blocking?`
2087
+ : intent.role === "verifier"
2088
+ ? "This sets the room verifier role. Use run verification when you want Waterbrother to run tests or build checks."
2089
+ : "";
2090
+ const followUp = intent.action === "agent-review" || intent.role === "reviewer"
2091
+ ? `Should ${getAgentOwnerDisplay(targetAgent, actor.displayName || actor.userId || "that terminal")} review be advisory or blocking?`
1973
2092
  : "";
1974
2093
  return {
1975
2094
  kind: "agent",
1976
2095
  project: nextProject,
1977
2096
  markup: [
1978
2097
  `<b>${escapeTelegramHtml(actionTitle)}</b>`,
1979
- `owner: <code>${escapeTelegramHtml(targetAgent.ownerName || targetAgent.ownerId || targetAgent.label || targetAgent.id || "-")}</code>`,
1980
- `terminal: <code>${escapeTelegramHtml(targetAgent.label || targetAgent.id || "-")}</code>`,
2098
+ `owner: <code>${escapeTelegramHtml(getAgentOwnerDisplay(targetAgent, actor.displayName || actor.userId || "current operator"))}</code>`,
2099
+ `terminal: <code>${escapeTelegramHtml(getAgentTerminalDisplay(targetAgent, actor.displayName ? actor.displayName + " terminal" : "current terminal"))}</code>`,
1981
2100
  `role: <code>${escapeTelegramHtml(intent.role)}</code>`,
1982
2101
  `runtime: <code>${escapeTelegramHtml(runtimeLabel)}</code>`,
1983
2102
  `project: <code>${escapeTelegramHtml(nextProject.projectName || path.basename(session.cwd || this.cwd))}</code>`,
@@ -2245,6 +2364,7 @@ class TelegramGateway {
2245
2364
  runtimeProfile: host?.runtimeProfile || project?.runtimeProfile || this.channel.defaultRuntimeProfile || this.gateway.defaultRuntimeProfile || "",
2246
2365
  hostSessionId: host?.sessionId || ""
2247
2366
  };
2367
+ const verifier = chooseVerifierAgent(project);
2248
2368
 
2249
2369
  if (intent.action === "bot-status") {
2250
2370
  const lines = [
@@ -2272,6 +2392,68 @@ class TelegramGateway {
2272
2392
  ].join("\n");
2273
2393
  }
2274
2394
 
2395
+ if (intent.action === "verifier-status") {
2396
+ if (!project?.enabled) {
2397
+ return "This project is not shared.";
2398
+ }
2399
+ if (!verifier) {
2400
+ return [
2401
+ "<b>Verifier</b>",
2402
+ "No verifier is assigned yet.",
2403
+ "Try: <code>make this terminal the verifier</code>"
2404
+ ].join("\n");
2405
+ }
2406
+ return [
2407
+ "<b>Verifier</b>",
2408
+ `verifier: <code>${escapeTelegramHtml(getAgentOwnerDisplay(verifier))}</code>`,
2409
+ `role: <code>${escapeTelegramHtml(verifier.role || "verifier")}</code>`,
2410
+ `runtime: <code>${escapeTelegramHtml(verifier.provider && verifier.model ? `${verifier.provider}/${verifier.model}` : "unknown")}</code>`,
2411
+ `verification mode: <code>${escapeTelegramHtml(String(project.verificationMode || "manual"))}</code>`
2412
+ ].join("\n");
2413
+ }
2414
+
2415
+ if (intent.action === "verification-mode-status") {
2416
+ if (!project?.enabled) {
2417
+ return "This project is not shared.";
2418
+ }
2419
+ return [
2420
+ "<b>Verification mode</b>",
2421
+ `mode: <code>${escapeTelegramHtml(String(project.verificationMode || "manual"))}</code>`
2422
+ ].join("\n");
2423
+ }
2424
+
2425
+ if (intent.action === "verification-mode-set") {
2426
+ if (!project?.enabled) {
2427
+ return "This project is not shared.";
2428
+ }
2429
+ const next = await setSharedVerificationMode(cwd, intent.mode, { actorId, actorName });
2430
+ return [
2431
+ "<b>Verification mode</b>",
2432
+ `mode: <code>${escapeTelegramHtml(String(next.verificationMode || intent.mode || "manual"))}</code>`
2433
+ ].join("\n");
2434
+ }
2435
+
2436
+ if (intent.action === "verification-status") {
2437
+ if (!project?.enabled) {
2438
+ return "This project is not shared.";
2439
+ }
2440
+ const result = getLatestVerificationResult(project);
2441
+ if (!result) {
2442
+ return "<b>Verification result</b>\nNo verification result is on record yet.";
2443
+ }
2444
+ return formatVerificationResultMarkup(result, verifier);
2445
+ }
2446
+
2447
+ if (intent.action === "run-verification") {
2448
+ if (!project?.enabled) {
2449
+ return "This project is not shared.";
2450
+ }
2451
+ const result = await this.runLocalVerification(cwd, verifier);
2452
+ const nextProject = await addVerificationResult(cwd, result, { actorId, actorName });
2453
+ const latest = getLatestVerificationResult(nextProject) || result;
2454
+ return formatVerificationResultMarkup(latest, verifier);
2455
+ }
2456
+
2275
2457
  if (intent.action === "accept-reviewer-concerns") {
2276
2458
  if (!project?.enabled) {
2277
2459
  return "This project is not shared.";
@@ -3146,6 +3328,75 @@ class TelegramGateway {
3146
3328
  return JSON.parse(stdout);
3147
3329
  }
3148
3330
 
3331
+ async planVerificationCommands(cwd) {
3332
+ const packageJsonPath = path.join(cwd, "package.json");
3333
+ let scripts = {};
3334
+ try {
3335
+ const raw = await fs.readFile(packageJsonPath, "utf8");
3336
+ const parsed = JSON.parse(raw);
3337
+ scripts = parsed?.scripts && typeof parsed.scripts === "object" ? parsed.scripts : {};
3338
+ } catch {}
3339
+ const commands = [];
3340
+ if (scripts.check) commands.push(["npm", ["run", "check"], "npm run check"]);
3341
+ if (scripts.test) commands.push(["npm", ["test"], "npm test"]);
3342
+ if (scripts.build) commands.push(["npm", ["run", "build"], "npm run build"]);
3343
+ if (!commands.length) {
3344
+ throw new Error("No verification commands are available here. Expected package.json scripts like check, test, or build.");
3345
+ }
3346
+ return commands;
3347
+ }
3348
+
3349
+ async runLocalVerification(cwd, verifier = null) {
3350
+ const planned = await this.planVerificationCommands(cwd);
3351
+ const commands = [];
3352
+ const startedAt = new Date().toISOString();
3353
+ const logs = [];
3354
+ let passedCount = 0;
3355
+ for (const [bin, args, label] of planned) {
3356
+ commands.push(label);
3357
+ try {
3358
+ await execFileAsync(bin, args, {
3359
+ cwd,
3360
+ env: { ...process.env },
3361
+ maxBuffer: 8 * 1024 * 1024,
3362
+ timeout: 10 * 60 * 1000
3363
+ });
3364
+ passedCount += 1;
3365
+ } catch (error) {
3366
+ const combined = `${String(error?.stderr || "")}\n${String(error?.stdout || "")}`.trim();
3367
+ const extracted = combined
3368
+ .split("\n")
3369
+ .map((line) => String(line || "").trim())
3370
+ .filter(Boolean)
3371
+ .slice(0, 6);
3372
+ logs.push(...extracted);
3373
+ const outcome = error?.killed || error?.signal === "SIGTERM" ? "timeout" : (passedCount > 0 ? "partial" : "failed");
3374
+ return {
3375
+ id: "",
3376
+ workItemId: "",
3377
+ verifierAgentId: String(verifier?.id || "").trim(),
3378
+ outcome,
3379
+ summary: `${label} failed${passedCount > 0 ? ` after ${passedCount} passing check${passedCount === 1 ? "" : "s"}` : ""}.`,
3380
+ commands,
3381
+ startedAt,
3382
+ completedAt: new Date().toISOString(),
3383
+ logs
3384
+ };
3385
+ }
3386
+ }
3387
+ return {
3388
+ id: "",
3389
+ workItemId: "",
3390
+ verifierAgentId: String(verifier?.id || "").trim(),
3391
+ outcome: "passed",
3392
+ summary: `${commands.length} verification command${commands.length === 1 ? "" : "s"} passed.`,
3393
+ commands,
3394
+ startedAt,
3395
+ completedAt: new Date().toISOString(),
3396
+ logs: []
3397
+ };
3398
+ }
3399
+
3149
3400
  async startTypingLoop(chatId) {
3150
3401
  let stopped = false;
3151
3402
  const tick = async () => {
@@ -8,7 +8,9 @@ const ROUNDTABLE_FILE = "ROUNDTABLE.md";
8
8
  const TASK_STATES = ["open", "active", "blocked", "done"];
9
9
  const RECENT_EVENT_LIMIT = 24;
10
10
  const PARTICIPANT_ROLES = ["owner", "editor", "observer"];
11
- const AGENT_ROLES = ["executor", "reviewer", "standby", "coordinator"];
11
+ const AGENT_ROLES = ["executor", "reviewer", "verifier", "standby", "coordinator"];
12
+ const VERIFICATION_MODES = ["off", "manual", "auto", "blocking"];
13
+ const VERIFICATION_RESULT_LIMIT = 50;
12
14
 
13
15
  function makeId(prefix = "id") {
14
16
  return `${prefix}_${crypto.randomBytes(3).toString("hex")}`;
@@ -73,6 +75,37 @@ function normalizeAgent(agent = {}) {
73
75
  };
74
76
  }
75
77
 
78
+ function normalizeVerificationResult(result = {}) {
79
+ const commands = Array.isArray(result.commands)
80
+ ? result.commands.map((item) => String(item || "").trim()).filter(Boolean)
81
+ : [];
82
+ const logs = Array.isArray(result.logs)
83
+ ? result.logs.map((item) => String(item || "").trim()).filter(Boolean)
84
+ : [];
85
+ const artifacts = Array.isArray(result.artifacts)
86
+ ? result.artifacts
87
+ .map((artifact) => ({
88
+ label: String(artifact?.label || "").trim(),
89
+ path: String(artifact?.path || "").trim(),
90
+ url: String(artifact?.url || "").trim()
91
+ }))
92
+ .filter((artifact) => artifact.label || artifact.path || artifact.url)
93
+ : [];
94
+ const outcome = String(result.outcome || "").trim().toLowerCase();
95
+ return {
96
+ id: String(result.id || makeId("vr")).trim(),
97
+ workItemId: String(result.workItemId || "").trim(),
98
+ verifierAgentId: String(result.verifierAgentId || "").trim(),
99
+ outcome: ["passed", "failed", "partial", "timeout"].includes(outcome) ? outcome : "failed",
100
+ summary: String(result.summary || "").trim(),
101
+ commands,
102
+ startedAt: String(result.startedAt || new Date().toISOString()).trim(),
103
+ completedAt: String(result.completedAt || result.startedAt || new Date().toISOString()).trim(),
104
+ logs,
105
+ artifacts
106
+ };
107
+ }
108
+
76
109
  function isBrokenAgent(agent = {}) {
77
110
  const ownerId = String(agent.ownerId || "").trim();
78
111
  const ownerName = String(agent.ownerName || "").trim();
@@ -217,6 +250,12 @@ function normalizeSharedProject(project = {}, cwd = process.cwd()) {
217
250
  : null;
218
251
  const participants = buildProjectParticipants({ ...project, members });
219
252
  const agents = buildProjectAgents({ ...project, members, activeOperator });
253
+ const verificationResults = Array.isArray(project.verificationResults)
254
+ ? project.verificationResults
255
+ .map((item) => normalizeVerificationResult(item))
256
+ .filter((item) => item.id)
257
+ .slice(-VERIFICATION_RESULT_LIMIT)
258
+ : [];
220
259
 
221
260
  return {
222
261
  version: 1,
@@ -233,10 +272,15 @@ function normalizeSharedProject(project = {}, cwd = process.cwd()) {
233
272
  roomMode: ["chat", "plan", "execute"].includes(String(project.roomMode || "").trim())
234
273
  ? String(project.roomMode).trim()
235
274
  : "chat",
275
+ verificationMode: VERIFICATION_MODES.includes(String(project.verificationMode || "").trim())
276
+ ? String(project.verificationMode).trim()
277
+ : "manual",
236
278
  runtimeProfile: String(project.runtimeProfile || "").trim(),
237
279
  members,
238
280
  participants,
239
281
  agents,
282
+ verificationResults,
283
+ latestVerificationResultId: String(project.latestVerificationResultId || "").trim(),
240
284
  tasks,
241
285
  pendingInvites,
242
286
  recentEvents,
@@ -576,6 +620,25 @@ export async function setSharedRuntimeProfile(cwd, runtimeProfile = "", options
576
620
  })).project;
577
621
  }
578
622
 
623
+ export async function setSharedVerificationMode(cwd, verificationMode = "manual", options = {}) {
624
+ const existing = await loadSharedProject(cwd);
625
+ requireOwner(existing, options.actorId);
626
+ const normalized = String(verificationMode || "").trim().toLowerCase();
627
+ if (!VERIFICATION_MODES.includes(normalized)) {
628
+ throw new Error(`Invalid verification mode. Expected one of ${VERIFICATION_MODES.join(", ")}.`);
629
+ }
630
+ const next = await saveSharedProject(cwd, {
631
+ ...existing,
632
+ verificationMode: normalized
633
+ });
634
+ return (await recordSharedProjectEvent(cwd, next, `verification mode set to ${normalized}`, {
635
+ type: "verification-mode",
636
+ actorId: String(options.actorId || "").trim(),
637
+ actorName: String(options.actorName || "").trim(),
638
+ meta: { verificationMode: normalized }
639
+ })).project;
640
+ }
641
+
579
642
  export function getSharedMember(project, memberId = "") {
580
643
  const normalizedId = String(memberId || "").trim();
581
644
  if (!normalizedId || !project?.members?.length) return null;
@@ -702,6 +765,46 @@ export async function upsertSharedAgent(cwd, agent = {}, options = {}) {
702
765
  })).project;
703
766
  }
704
767
 
768
+ export function getLatestVerificationResult(project) {
769
+ const latestId = String(project?.latestVerificationResultId || "").trim();
770
+ const results = Array.isArray(project?.verificationResults) ? project.verificationResults : [];
771
+ if (latestId) {
772
+ const exact = results.find((result) => String(result?.id || "").trim() === latestId);
773
+ if (exact) return exact;
774
+ }
775
+ return [...results].sort((a, b) => String(b.completedAt || "").localeCompare(String(a.completedAt || "")))[0] || null;
776
+ }
777
+
778
+ export async function addVerificationResult(cwd, result = {}, options = {}) {
779
+ const existing = await loadSharedProject(cwd);
780
+ requireSharedProject(existing);
781
+ if (options.actorId) {
782
+ requireMember(existing, options.actorId);
783
+ }
784
+ const verificationResult = normalizeVerificationResult(result);
785
+ const verificationResults = [...(existing.verificationResults || []), verificationResult].slice(-VERIFICATION_RESULT_LIMIT);
786
+ const next = await saveSharedProject(cwd, {
787
+ ...existing,
788
+ verificationResults,
789
+ latestVerificationResultId: verificationResult.id
790
+ });
791
+ return (await recordSharedProjectEvent(
792
+ cwd,
793
+ next,
794
+ `verification ${verificationResult.outcome} [${verificationResult.id}] ${verificationResult.summary || "result recorded"}`.trim(),
795
+ {
796
+ type: "verification-result",
797
+ actorId: String(options.actorId || "").trim(),
798
+ actorName: String(options.actorName || "").trim(),
799
+ meta: {
800
+ verificationResultId: verificationResult.id,
801
+ verifierAgentId: verificationResult.verifierAgentId,
802
+ outcome: verificationResult.outcome
803
+ }
804
+ }
805
+ )).project;
806
+ }
807
+
705
808
  export async function addSharedRoomNote(cwd, text = "", options = {}) {
706
809
  const existing = await loadSharedProject(cwd);
707
810
  requireSharedProject(existing);
@@ -1123,12 +1226,15 @@ export function formatSharedProjectStatus(project) {
1123
1226
  room: project.room,
1124
1227
  mode: project.mode,
1125
1228
  roomMode: project.roomMode,
1229
+ verificationMode: project.verificationMode,
1126
1230
  runtimeProfile: project.runtimeProfile,
1127
1231
  approvalPolicy: project.approvalPolicy,
1128
1232
  activeOperator: project.activeOperator,
1129
1233
  members: project.members,
1130
1234
  participants: project.participants,
1131
1235
  agents: project.agents,
1236
+ verificationResults: project.verificationResults,
1237
+ latestVerificationResultId: project.latestVerificationResultId,
1132
1238
  pendingInvites: project.pendingInvites,
1133
1239
  tasks: project.tasks,
1134
1240
  recentEvents: project.recentEvents