@tritard/waterbrother 0.16.97 → 0.16.98

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.98",
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
 
@@ -526,6 +533,11 @@ function chooseReviewerAgent(project) {
526
533
  return agents.find((agent) => String(agent?.role || "").trim() === "reviewer") || null;
527
534
  }
528
535
 
536
+ function chooseVerifierAgent(project) {
537
+ const agents = listProjectAgents(project);
538
+ return agents.find((agent) => String(agent?.role || "").trim() === "verifier") || null;
539
+ }
540
+
529
541
  function summarizeExecutorReviewerArbitration(project, fallbackExecutor = {}) {
530
542
  const executorAgent = chooseExecutorAgent(project, fallbackExecutor);
531
543
  const reviewerAgent = chooseReviewerAgent(project);
@@ -541,6 +553,29 @@ function summarizeExecutorReviewerArbitration(project, fallbackExecutor = {}) {
541
553
  };
542
554
  }
543
555
 
556
+ function formatVerificationResultMarkup(result = {}, verifier = null) {
557
+ const lines = [
558
+ "<b>Verification result</b>",
559
+ `outcome: <code>${escapeTelegramHtml(String(result.outcome || "failed"))}</code>`
560
+ ];
561
+ if (verifier) {
562
+ lines.splice(1, 0, `verifier: <code>${escapeTelegramHtml(verifier.ownerName || verifier.label || verifier.ownerId || verifier.id || "unknown")}</code>`);
563
+ }
564
+ if (Array.isArray(result.commands) && result.commands.length) {
565
+ lines.push("commands:");
566
+ for (const command of result.commands) {
567
+ lines.push(`• <code>${escapeTelegramHtml(command)}</code>`);
568
+ }
569
+ }
570
+ if (result.summary) {
571
+ lines.push("", "<b>Summary</b>", escapeTelegramHtml(result.summary));
572
+ }
573
+ if (Array.isArray(result.logs) && result.logs.length) {
574
+ lines.push("", "<b>Top errors</b>", ...result.logs.slice(0, 6).map((line) => `• ${escapeTelegramHtml(line)}`));
575
+ }
576
+ return lines.filter(Boolean).join("\n");
577
+ }
578
+
544
579
  function parseReviewerOutcome(text = "") {
545
580
  const value = String(text || "").trim();
546
581
  const lower = value.toLowerCase();
@@ -783,15 +818,15 @@ function parseTelegramAgentIntent(text = "") {
783
818
  }
784
819
  }
785
820
 
786
- const roleMatch = lowered.match(/\b(executor|reviewer|standby)\b/);
821
+ const roleMatch = lowered.match(/\b(executor|reviewer|verifier|standby)\b/);
787
822
  const role = roleMatch?.[1] || "";
788
823
  if (!role) return null;
789
824
 
790
825
  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
826
+ /^(?:have|make|set)\s+(.+?)['’]s\s+(?:bot|terminal)\s+(?:be|as|to)?\s*(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i,
827
+ /^(?:have|make|set)\s+(.+?)\s+(?:bot|terminal)\s+(?:be|as|to)?\s*(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i,
828
+ /^(.+?)['’]s\s+(?:bot|terminal)\s+should\s+(?:be\s+)?(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i,
829
+ /^(.+?)\s+(?:bot|terminal)\s+should\s+(?:be\s+)?(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i
795
830
  ];
796
831
  for (const pattern of patterns) {
797
832
  const match = value.match(pattern);
@@ -834,9 +869,28 @@ function parseTelegramStateIntent(text = "") {
834
869
  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
870
  return { action: "room-guidance" };
836
871
  }
872
+ if (/\bwho is the verifier\b/.test(lower) || /\bwhat is the verifier\b/.test(lower) || /\bwho should verify this\b/.test(lower)) {
873
+ return { action: "verifier-status" };
874
+ }
837
875
  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
876
  return { action: "agent-list" };
839
877
  }
878
+ 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)) {
879
+ return { action: /\bshow latest verification result\b/.test(lower) || /\bdid verification pass\b/.test(lower) ? "verification-status" : "run-verification" };
880
+ }
881
+ if (/\bverification mode\b/.test(lower)) {
882
+ const modeMatch = lower.match(/\b(off|manual|auto|blocking)\b/);
883
+ return modeMatch?.[1] ? { action: "verification-mode-set", mode: modeMatch[1] } : { action: "verification-mode-status" };
884
+ }
885
+ if (/\balways verify after execution\b/.test(lower)) {
886
+ return { action: "verification-mode-set", mode: "auto" };
887
+ }
888
+ if (/\bturn off verification\b/.test(lower)) {
889
+ return { action: "verification-mode-set", mode: "off" };
890
+ }
891
+ if (/\bverification should be blocking\b/.test(lower)) {
892
+ return { action: "verification-mode-set", mode: "blocking" };
893
+ }
840
894
  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
895
  return { action: "live-hosts" };
842
896
  }
@@ -980,11 +1034,14 @@ function formatTelegramRoomMarkup(project, options = {}) {
980
1034
  const liveHosts = Array.isArray(options.liveHosts) ? options.liveHosts : [];
981
1035
  const selectedExecutor = chooseExecutorAgent(project, executor);
982
1036
  const selectedReviewer = chooseReviewerAgent(project);
1037
+ const selectedVerifier = chooseVerifierAgent(project);
983
1038
  const blockingReview = getLatestBlockingReviewPolicy(project);
984
1039
  const latestReviewResult = getLatestReviewResult(project);
985
1040
  const latestReviewMeta = latestReviewResult?.meta && typeof latestReviewResult.meta === "object" ? latestReviewResult.meta : {};
1041
+ const latestVerificationResult = getLatestVerificationResult(project);
986
1042
  const selectedExecutorLiveHost = selectedExecutor ? findLiveHostForAgent(liveHosts, selectedExecutor) : null;
987
1043
  const selectedReviewerLiveHost = selectedReviewer ? findLiveHostForAgent(liveHosts, selectedReviewer) : null;
1044
+ const selectedVerifierLiveHost = selectedVerifier ? findLiveHostForAgent(liveHosts, selectedVerifier) : null;
988
1045
  const executorBits = [
989
1046
  `surface: <code>${escapeTelegramHtml(executor.surface || "telegram")}</code>`,
990
1047
  `provider: <code>${escapeTelegramHtml(executor.provider || "unknown")}</code>`,
@@ -1012,8 +1069,12 @@ function formatTelegramRoomMarkup(project, options = {}) {
1012
1069
  `executor live: <code>${escapeTelegramHtml(selectedExecutorLiveHost ? "yes" : "no")}</code>`,
1013
1070
  `reviewer: <code>${escapeTelegramHtml(selectedReviewer ? (selectedReviewer.ownerName || selectedReviewer.label || selectedReviewer.ownerId || selectedReviewer.id || "none") : "none")}</code>`,
1014
1071
  `reviewer live: <code>${escapeTelegramHtml(selectedReviewerLiveHost ? "yes" : "no")}</code>`,
1072
+ `verifier: <code>${escapeTelegramHtml(selectedVerifier ? (selectedVerifier.ownerName || selectedVerifier.label || selectedVerifier.ownerId || selectedVerifier.id || "none") : "none")}</code>`,
1073
+ `verifier live: <code>${escapeTelegramHtml(selectedVerifierLiveHost ? "yes" : "no")}</code>`,
1015
1074
  `blocking review: <code>${escapeTelegramHtml(blockingReview ? "yes" : "no")}</code>`,
1016
1075
  `latest reviewer outcome: <code>${escapeTelegramHtml(String(latestReviewMeta.outcome || "").trim() || "none")}</code>`,
1076
+ `verification mode: <code>${escapeTelegramHtml(String(project.verificationMode || "manual"))}</code>`,
1077
+ `latest verification outcome: <code>${escapeTelegramHtml(String(latestVerificationResult?.outcome || "").trim() || "none")}</code>`,
1017
1078
  "<b>Executor</b>",
1018
1079
  ...executorBits,
1019
1080
  "<b>Runtime Split</b>",
@@ -1946,7 +2007,31 @@ class TelegramGateway {
1946
2007
  throw new Error("Only a shared-project owner can change terminal roles.");
1947
2008
  }
1948
2009
  const actor = this.describeTelegramUser(message?.from || {});
1949
- const targetAgent = resolveProjectAgent(project, intent.target);
2010
+ const normalizedTarget = String(intent.target || "").trim().toLowerCase();
2011
+ let targetAgent = resolveProjectAgent(project, intent.target);
2012
+ if (!targetAgent && /^(?:this|my)(?:\s+(?:bot|terminal))?$/.test(normalizedTarget)) {
2013
+ const host = await this.getLiveBridgeHost();
2014
+ if (host) {
2015
+ targetAgent = listProjectAgents(project).find((agent) => (
2016
+ String(agent?.sessionId || "").trim() && String(agent.sessionId || "").trim() === String(host.sessionId || "").trim()
2017
+ ) || (
2018
+ String(agent?.ownerId || "").trim() && String(agent.ownerId || "").trim() === String(host.ownerId || "").trim()
2019
+ )) || {
2020
+ id: `agent:telegram-bridge:${String(host.sessionId || host.pid || "current").trim()}`,
2021
+ ownerId: String(host.ownerId || actor.userId || "").trim(),
2022
+ ownerName: String(host.ownerName || actor.displayName || "").trim(),
2023
+ label: String(host.label || `${actor.displayName || actor.userId || "current"} terminal`).trim(),
2024
+ surface: String(host.surface || "live-tui").trim(),
2025
+ role: "standby",
2026
+ provider: String(host.provider || "").trim(),
2027
+ model: String(host.model || "").trim(),
2028
+ runtimeProfile: String(host.runtimeProfile || "").trim(),
2029
+ sessionId: String(host.sessionId || "").trim(),
2030
+ cwd: String(session.cwd || this.cwd).trim(),
2031
+ chatId: String(message?.chat?.id || "").trim()
2032
+ };
2033
+ }
2034
+ }
1950
2035
  if (!targetAgent) {
1951
2036
  throw new Error(`No terminal found for ${intent.target}. Ask them to connect their Waterbrother bot/terminal first.`);
1952
2037
  }
@@ -1958,17 +2043,21 @@ class TelegramGateway {
1958
2043
  actorName: actor.displayName || actorId
1959
2044
  });
1960
2045
  const runtimeLabel = targetAgent.provider && targetAgent.model ? `${targetAgent.provider}/${targetAgent.model}` : "unknown";
1961
- const actionTitle = intent.action === "agent-review"
2046
+ const actionTitle = intent.action === "agent-review" || intent.role === "reviewer"
1962
2047
  ? "Roundtable reviewer assigned"
1963
- : intent.action === "agent-execute"
2048
+ : intent.action === "agent-execute" || intent.role === "executor"
1964
2049
  ? "Roundtable executor assigned"
1965
- : "Roundtable terminal updated";
1966
- const note = intent.action === "agent-review"
2050
+ : intent.role === "verifier"
2051
+ ? "Roundtable verifier assigned"
2052
+ : "Roundtable terminal updated";
2053
+ const note = intent.action === "agent-review" || intent.role === "reviewer"
1967
2054
  ? "This sets the room reviewer role. It does not automatically run a review pass yet."
1968
- : intent.action === "agent-execute"
2055
+ : intent.action === "agent-execute" || intent.role === "executor"
1969
2056
  ? "This sets the room executor role. Claim/mode rules still control actual execution."
1970
- : "";
1971
- const followUp = intent.action === "agent-review"
2057
+ : intent.role === "verifier"
2058
+ ? "This sets the room verifier role. Use run verification when you want Waterbrother to run tests or build checks."
2059
+ : "";
2060
+ const followUp = intent.action === "agent-review" || intent.role === "reviewer"
1972
2061
  ? `Should ${targetAgent.ownerName || targetAgent.label || "that terminal"} review be advisory or blocking?`
1973
2062
  : "";
1974
2063
  return {
@@ -2245,6 +2334,7 @@ class TelegramGateway {
2245
2334
  runtimeProfile: host?.runtimeProfile || project?.runtimeProfile || this.channel.defaultRuntimeProfile || this.gateway.defaultRuntimeProfile || "",
2246
2335
  hostSessionId: host?.sessionId || ""
2247
2336
  };
2337
+ const verifier = chooseVerifierAgent(project);
2248
2338
 
2249
2339
  if (intent.action === "bot-status") {
2250
2340
  const lines = [
@@ -2272,6 +2362,68 @@ class TelegramGateway {
2272
2362
  ].join("\n");
2273
2363
  }
2274
2364
 
2365
+ if (intent.action === "verifier-status") {
2366
+ if (!project?.enabled) {
2367
+ return "This project is not shared.";
2368
+ }
2369
+ if (!verifier) {
2370
+ return [
2371
+ "<b>Verifier</b>",
2372
+ "No verifier is assigned yet.",
2373
+ "Try: <code>make this terminal the verifier</code>"
2374
+ ].join("\n");
2375
+ }
2376
+ return [
2377
+ "<b>Verifier</b>",
2378
+ `verifier: <code>${escapeTelegramHtml(verifier.ownerName || verifier.label || verifier.ownerId || verifier.id || "unknown")}</code>`,
2379
+ `role: <code>${escapeTelegramHtml(verifier.role || "verifier")}</code>`,
2380
+ `runtime: <code>${escapeTelegramHtml(verifier.provider && verifier.model ? `${verifier.provider}/${verifier.model}` : "unknown")}</code>`,
2381
+ `verification mode: <code>${escapeTelegramHtml(String(project.verificationMode || "manual"))}</code>`
2382
+ ].join("\n");
2383
+ }
2384
+
2385
+ if (intent.action === "verification-mode-status") {
2386
+ if (!project?.enabled) {
2387
+ return "This project is not shared.";
2388
+ }
2389
+ return [
2390
+ "<b>Verification mode</b>",
2391
+ `mode: <code>${escapeTelegramHtml(String(project.verificationMode || "manual"))}</code>`
2392
+ ].join("\n");
2393
+ }
2394
+
2395
+ if (intent.action === "verification-mode-set") {
2396
+ if (!project?.enabled) {
2397
+ return "This project is not shared.";
2398
+ }
2399
+ const next = await setSharedVerificationMode(cwd, intent.mode, { actorId, actorName });
2400
+ return [
2401
+ "<b>Verification mode</b>",
2402
+ `mode: <code>${escapeTelegramHtml(String(next.verificationMode || intent.mode || "manual"))}</code>`
2403
+ ].join("\n");
2404
+ }
2405
+
2406
+ if (intent.action === "verification-status") {
2407
+ if (!project?.enabled) {
2408
+ return "This project is not shared.";
2409
+ }
2410
+ const result = getLatestVerificationResult(project);
2411
+ if (!result) {
2412
+ return "<b>Verification result</b>\nNo verification result is on record yet.";
2413
+ }
2414
+ return formatVerificationResultMarkup(result, verifier);
2415
+ }
2416
+
2417
+ if (intent.action === "run-verification") {
2418
+ if (!project?.enabled) {
2419
+ return "This project is not shared.";
2420
+ }
2421
+ const result = await this.runLocalVerification(cwd, verifier);
2422
+ const nextProject = await addVerificationResult(cwd, result, { actorId, actorName });
2423
+ const latest = getLatestVerificationResult(nextProject) || result;
2424
+ return formatVerificationResultMarkup(latest, verifier);
2425
+ }
2426
+
2275
2427
  if (intent.action === "accept-reviewer-concerns") {
2276
2428
  if (!project?.enabled) {
2277
2429
  return "This project is not shared.";
@@ -3146,6 +3298,75 @@ class TelegramGateway {
3146
3298
  return JSON.parse(stdout);
3147
3299
  }
3148
3300
 
3301
+ async planVerificationCommands(cwd) {
3302
+ const packageJsonPath = path.join(cwd, "package.json");
3303
+ let scripts = {};
3304
+ try {
3305
+ const raw = await fs.readFile(packageJsonPath, "utf8");
3306
+ const parsed = JSON.parse(raw);
3307
+ scripts = parsed?.scripts && typeof parsed.scripts === "object" ? parsed.scripts : {};
3308
+ } catch {}
3309
+ const commands = [];
3310
+ if (scripts.check) commands.push(["npm", ["run", "check"], "npm run check"]);
3311
+ if (scripts.test) commands.push(["npm", ["test"], "npm test"]);
3312
+ if (scripts.build) commands.push(["npm", ["run", "build"], "npm run build"]);
3313
+ if (!commands.length) {
3314
+ throw new Error("No verification commands are available here. Expected package.json scripts like check, test, or build.");
3315
+ }
3316
+ return commands;
3317
+ }
3318
+
3319
+ async runLocalVerification(cwd, verifier = null) {
3320
+ const planned = await this.planVerificationCommands(cwd);
3321
+ const commands = [];
3322
+ const startedAt = new Date().toISOString();
3323
+ const logs = [];
3324
+ let passedCount = 0;
3325
+ for (const [bin, args, label] of planned) {
3326
+ commands.push(label);
3327
+ try {
3328
+ await execFileAsync(bin, args, {
3329
+ cwd,
3330
+ env: { ...process.env },
3331
+ maxBuffer: 8 * 1024 * 1024,
3332
+ timeout: 10 * 60 * 1000
3333
+ });
3334
+ passedCount += 1;
3335
+ } catch (error) {
3336
+ const combined = `${String(error?.stderr || "")}\n${String(error?.stdout || "")}`.trim();
3337
+ const extracted = combined
3338
+ .split("\n")
3339
+ .map((line) => String(line || "").trim())
3340
+ .filter(Boolean)
3341
+ .slice(0, 6);
3342
+ logs.push(...extracted);
3343
+ const outcome = error?.killed || error?.signal === "SIGTERM" ? "timeout" : (passedCount > 0 ? "partial" : "failed");
3344
+ return {
3345
+ id: "",
3346
+ workItemId: "",
3347
+ verifierAgentId: String(verifier?.id || "").trim(),
3348
+ outcome,
3349
+ summary: `${label} failed${passedCount > 0 ? ` after ${passedCount} passing check${passedCount === 1 ? "" : "s"}` : ""}.`,
3350
+ commands,
3351
+ startedAt,
3352
+ completedAt: new Date().toISOString(),
3353
+ logs
3354
+ };
3355
+ }
3356
+ }
3357
+ return {
3358
+ id: "",
3359
+ workItemId: "",
3360
+ verifierAgentId: String(verifier?.id || "").trim(),
3361
+ outcome: "passed",
3362
+ summary: `${commands.length} verification command${commands.length === 1 ? "" : "s"} passed.`,
3363
+ commands,
3364
+ startedAt,
3365
+ completedAt: new Date().toISOString(),
3366
+ logs: []
3367
+ };
3368
+ }
3369
+
3149
3370
  async startTypingLoop(chatId) {
3150
3371
  let stopped = false;
3151
3372
  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