@tritard/waterbrother 0.16.141 → 0.16.142

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/discord.js +257 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.141",
3
+ "version": "0.16.142",
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/discord.js CHANGED
@@ -5,6 +5,8 @@ import { buildSelfAwarenessManifest, formatAboutWaterbrother, formatSelfState }
5
5
  import { createSession, listSessions, loadSession, saveSession } from "./session-store.js";
6
6
  import { loadGatewayBridge, loadGatewayState, saveGatewayBridge, saveGatewayState } from "./gateway-state.js";
7
7
  import {
8
+ addSharedRoomNote,
9
+ addVerificationResult,
8
10
  acceptSharedInvite,
9
11
  approveSharedInvite,
10
12
  assignSharedTask,
@@ -170,6 +172,14 @@ function buildDiscordHelp() {
170
172
  "/executor show the selected executor",
171
173
  "/reviewer show the assigned reviewer",
172
174
  "/verifier show the assigned verifier",
175
+ "/verify run verification through the assigned verifier or live terminal",
176
+ "/verification show the latest verification result",
177
+ "/rerun-verification rerun verification",
178
+ "/override-verification override the current blocking verification",
179
+ "/review-status show blocking review and latest reviewer outcome",
180
+ "/accept-reviewer-concerns clear blocking review by accepting the reviewer concerns",
181
+ "/override-reviewer override the current blocking reviewer outcome",
182
+ "/switch-executor-to-reviewer make the reviewer the executor",
173
183
  "/assign-role <terminal|this terminal> <executor|reviewer|verifier|standby> assign a room role",
174
184
  "/members list shared room members",
175
185
  "/tasks list shared room tasks",
@@ -399,6 +409,92 @@ function formatDiscordTaskHistory(result = {}) {
399
409
  return lines.join("\n");
400
410
  }
401
411
 
412
+ function getLatestBlockingReviewPolicy(project = {}) {
413
+ const events = Array.isArray(project?.recentEvents) ? [...project.recentEvents] : [];
414
+ const ordered = events
415
+ .filter((event) => event?.createdAt)
416
+ .sort((a, b) => String(b.createdAt || "").localeCompare(String(a.createdAt || "")));
417
+ const resolvedAgents = new Set();
418
+ for (const event of ordered) {
419
+ const type = String(event?.type || "").trim();
420
+ const meta = event?.meta && typeof event.meta === "object" ? event.meta : {};
421
+ const agentId = String(meta.agentId || "").trim();
422
+ if (!agentId) continue;
423
+ if (type === "review-policy-cleared" || type === "review-policy-override") {
424
+ resolvedAgents.add(agentId);
425
+ continue;
426
+ }
427
+ if (type === "review-policy" && String(meta.policy || "").trim() === "blocking" && !resolvedAgents.has(agentId)) {
428
+ return event;
429
+ }
430
+ }
431
+ return null;
432
+ }
433
+
434
+ function getLatestReviewResult(project = {}) {
435
+ const events = Array.isArray(project?.recentEvents) ? [...project.recentEvents] : [];
436
+ const ordered = events
437
+ .filter((event) => event?.createdAt)
438
+ .sort((a, b) => String(b.createdAt || "").localeCompare(String(a.createdAt || "")));
439
+ return ordered.find((event) => String(event?.type || "").trim() === "review-result") || null;
440
+ }
441
+
442
+ function getBlockingVerificationResult(project = {}) {
443
+ if (String(project?.verificationMode || "manual").trim() !== "blocking") {
444
+ return null;
445
+ }
446
+ const latestResult = getLatestVerificationResult(project);
447
+ const latestId = String(latestResult?.id || "").trim();
448
+ const latestOutcome = String(latestResult?.outcome || "").trim().toLowerCase();
449
+ if (!latestId || !latestOutcome || latestOutcome === "passed") return null;
450
+ const events = Array.isArray(project?.recentEvents) ? [...project.recentEvents] : [];
451
+ const ordered = events
452
+ .filter((event) => event?.createdAt)
453
+ .sort((a, b) => String(b.createdAt || "").localeCompare(String(a.createdAt || "")));
454
+ for (const event of ordered) {
455
+ const type = String(event?.type || "").trim();
456
+ const meta = event?.meta && typeof event.meta === "object" ? event.meta : {};
457
+ if (String(meta.verificationResultId || "").trim() !== latestId) continue;
458
+ if (type === "verification-override") return null;
459
+ if (type === "verification-result") return latestResult;
460
+ }
461
+ return latestResult;
462
+ }
463
+
464
+ function formatDiscordVerificationResult(result = {}, verifier = null) {
465
+ const lines = [
466
+ "Verification result",
467
+ `outcome: ${String(result.outcome || "failed")}`
468
+ ];
469
+ if (verifier) lines.splice(1, 0, `verifier: ${getAgentOwnerDisplay(verifier)}`);
470
+ if (result.failedCommand) lines.push(`failed step: ${String(result.failedCommand || "")}`);
471
+ if (result.isolation) lines.push(`environment: ${String(result.isolation || "")}`);
472
+ if (Array.isArray(result.commands) && result.commands.length) {
473
+ lines.push("commands:");
474
+ for (const command of result.commands) lines.push(`- ${command}`);
475
+ }
476
+ if (result.summary) lines.push("", "Summary", String(result.summary || ""));
477
+ if (Array.isArray(result.logs) && result.logs.length) {
478
+ lines.push("", "Top errors", ...result.logs.slice(0, 6).map((line) => `- ${line}`));
479
+ }
480
+ if (String(result.outcome || "").trim() !== "passed") {
481
+ lines.push("", "Next actions", "- /rerun-verification", "- /override-verification");
482
+ }
483
+ return lines.join("\n");
484
+ }
485
+
486
+ function formatDiscordReviewStatus(project = {}) {
487
+ const blockingReview = getLatestBlockingReviewPolicy(project);
488
+ const latestReview = getLatestReviewResult(project);
489
+ const reviewMeta = latestReview?.meta && typeof latestReview.meta === "object" ? latestReview.meta : {};
490
+ return [
491
+ "Reviewer status",
492
+ `blocking review: ${blockingReview ? "yes" : "no"}`,
493
+ `latest reviewer outcome: ${String(reviewMeta.outcome || "").trim() || "none"}`,
494
+ reviewMeta.agentId ? `reviewer agent: ${reviewMeta.agentId}` : ""
495
+ ].filter(Boolean).join("\n");
496
+ }
497
+
402
498
  function parseDiscordInviteCommand(message = {}, text = "") {
403
499
  const value = String(text || "").trim();
404
500
  const match = value.match(/^\/?(?:room\s+)?invite\s+(.+)$/i);
@@ -1049,7 +1145,163 @@ async function handleDiscordControlCommand(runtime, state, message, rawText) {
1049
1145
  const agent = chooseAgentByRole(project, "verifier");
1050
1146
  const liveHosts = await getLiveBridgeHosts({ cwd: session?.cwd || "" });
1051
1147
  const live = agent ? liveHosts.some((host) => String(host?.ownerId || "") === String(agent?.ownerId || "") || String(host?.sessionId || "") === String(agent?.sessionId || "")) : false;
1052
- return formatDiscordAgentStatus("Verifier", agent, { live });
1148
+ const latestVerification = getLatestVerificationResult(project);
1149
+ const blockingVerification = getBlockingVerificationResult(project);
1150
+ return [
1151
+ formatDiscordAgentStatus("Verifier", agent, { live }),
1152
+ `verification mode: ${String(project.verificationMode || "manual")}`,
1153
+ `blocking verification: ${blockingVerification ? "yes" : "no"}`,
1154
+ `latest verification outcome: ${String(latestVerification?.outcome || "").trim() || "none"}`
1155
+ ].filter(Boolean).join("\n");
1156
+ }
1157
+ if (normalized === "/verification" || normalized === "verification" || normalizedRoomAlias === "verification") {
1158
+ const sessionId = await ensurePeerSession(runtime, state, message);
1159
+ const { project } = await bindSharedRoomForMessage(message, sessionId);
1160
+ if (!project?.enabled) return "This project is not shared.";
1161
+ const verifier = chooseAgentByRole(project, "verifier");
1162
+ const result = getLatestVerificationResult(project);
1163
+ if (!result) return "Verification result\nNo verification result is on record yet.";
1164
+ return formatDiscordVerificationResult(result, verifier);
1165
+ }
1166
+ if (normalized === "/verify" || normalized === "verify" || normalized === "/run-verification" || normalizedRoomAlias === "verify" || normalizedRoomAlias === "run-verification") {
1167
+ const sessionId = await ensurePeerSession(runtime, state, message);
1168
+ const { project, session } = await bindSharedRoomForMessage(message, sessionId);
1169
+ if (!project?.enabled) return "This project is not shared.";
1170
+ const actor = describeDiscordUser(message);
1171
+ const verifier = chooseAgentByRole(project, "verifier");
1172
+ const liveHosts = await getLiveBridgeHosts({ cwd: session?.cwd || "" });
1173
+ const verifierHost = verifier ? liveHosts.find((host) => String(host?.ownerId || "") === String(verifier?.ownerId || "") || String(host?.sessionId || "") === String(verifier?.sessionId || "")) : null;
1174
+ const targetHost = verifierHost || await getLiveBridgeHost({ cwd: session?.cwd || "" });
1175
+ if (!targetHost) {
1176
+ return "No live Waterbrother TUI is connected. Start Waterbrother in the terminal first, then retry.";
1177
+ }
1178
+ const bridged = await runPromptViaBridge(runtime, message, "run verification", {
1179
+ sessionId,
1180
+ requestKind: "verification",
1181
+ targetHost
1182
+ });
1183
+ const routedResult = bridged?.meta?.verificationResult && typeof bridged.meta.verificationResult === "object"
1184
+ ? bridged.meta.verificationResult
1185
+ : null;
1186
+ if (routedResult) {
1187
+ const nextProject = await addVerificationResult(session.cwd || process.cwd(), routedResult, {
1188
+ actorId: actor.userId,
1189
+ actorName: actor.displayName
1190
+ });
1191
+ const latest = getLatestVerificationResult(nextProject) || routedResult;
1192
+ return formatDiscordVerificationResult(latest, verifier);
1193
+ }
1194
+ if (bridged?.error) return bridged.error;
1195
+ return String(bridged?.content || "").trim() || "Verification result\n(no content)";
1196
+ }
1197
+ if (normalized === "/rerun-verification" || normalized === "rerun verification" || normalizedRoomAlias === "rerun verification") {
1198
+ return handleDiscordControlCommand(runtime, state, message, "/verify");
1199
+ }
1200
+ if (normalized === "/override-verification" || normalized === "override verification" || normalizedRoomAlias === "override verification") {
1201
+ const sessionId = await ensurePeerSession(runtime, state, message);
1202
+ const { project, session } = await bindSharedRoomForMessage(message, sessionId);
1203
+ if (!project?.enabled) return "This project is not shared.";
1204
+ const blockingVerification = getBlockingVerificationResult(project);
1205
+ if (!blockingVerification) return "Verification override\nNo blocking verification is active.";
1206
+ const actor = describeDiscordUser(message);
1207
+ await addSharedRoomNote(session.cwd || process.cwd(), "Blocking verification overridden for now", {
1208
+ actorId: actor.userId,
1209
+ actorName: actor.displayName,
1210
+ type: "verification-override",
1211
+ meta: {
1212
+ verificationResultId: String(blockingVerification.id || "").trim(),
1213
+ outcome: String(blockingVerification.outcome || "").trim()
1214
+ }
1215
+ });
1216
+ return [
1217
+ "Verification overridden",
1218
+ `latest outcome: ${String(blockingVerification.outcome || "failed")}`,
1219
+ "Execution can proceed for now."
1220
+ ].join("\n");
1221
+ }
1222
+ if (normalized === "/review-status" || normalized === "review status" || normalizedRoomAlias === "review-status" || normalizedRoomAlias === "review status") {
1223
+ const sessionId = await ensurePeerSession(runtime, state, message);
1224
+ const { project } = await bindSharedRoomForMessage(message, sessionId);
1225
+ if (!project?.enabled) return "This project is not shared.";
1226
+ return formatDiscordReviewStatus(project);
1227
+ }
1228
+ if (normalized === "/accept-reviewer-concerns" || normalized === "accept reviewer concerns" || normalizedRoomAlias === "accept reviewer concerns") {
1229
+ const sessionId = await ensurePeerSession(runtime, state, message);
1230
+ const { project, session } = await bindSharedRoomForMessage(message, sessionId);
1231
+ if (!project?.enabled) return "This project is not shared.";
1232
+ const blockingReview = getLatestBlockingReviewPolicy(project);
1233
+ if (!blockingReview) return "Reviewer concerns\nNo blocking review is active.";
1234
+ const result = getLatestReviewResult(project);
1235
+ if (!result) return "Reviewer concerns\nNo reviewer result is on record yet.";
1236
+ const meta = result.meta && typeof result.meta === "object" ? result.meta : {};
1237
+ const blockingMeta = blockingReview.meta && typeof blockingReview.meta === "object" ? blockingReview.meta : {};
1238
+ if (String(meta.agentId || "").trim() !== String(blockingMeta.agentId || "").trim()) {
1239
+ return "Reviewer concerns\nThe latest reviewer result does not match the current blocking reviewer.";
1240
+ }
1241
+ if (String(meta.outcome || "").trim() !== "concerns") {
1242
+ return "Reviewer concerns\nThe latest reviewer result is not concerns.";
1243
+ }
1244
+ const actor = describeDiscordUser(message);
1245
+ await addSharedRoomNote(session.cwd || process.cwd(), "Reviewer concerns accepted; revise before continuing", {
1246
+ actorId: actor.userId,
1247
+ actorName: actor.displayName,
1248
+ type: "review-concerns-accepted",
1249
+ meta: { agentId: String(meta.agentId || "").trim() }
1250
+ });
1251
+ await addSharedRoomNote(session.cwd || process.cwd(), "Blocking review cleared after reviewer concerns were accepted", {
1252
+ actorId: actor.userId,
1253
+ actorName: actor.displayName,
1254
+ type: "review-policy-cleared",
1255
+ meta: { agentId: String(meta.agentId || "").trim() }
1256
+ });
1257
+ return "Reviewer concerns accepted\nBlocking review has been cleared so the team can revise and continue.";
1258
+ }
1259
+ if (normalized === "/override-reviewer" || normalized === "override reviewer" || normalizedRoomAlias === "override reviewer") {
1260
+ const sessionId = await ensurePeerSession(runtime, state, message);
1261
+ const { project, session } = await bindSharedRoomForMessage(message, sessionId);
1262
+ if (!project?.enabled) return "This project is not shared.";
1263
+ const blockingReview = getLatestBlockingReviewPolicy(project);
1264
+ if (!blockingReview) return "Reviewer override\nNo blocking review is active.";
1265
+ const result = getLatestReviewResult(project);
1266
+ const meta = result?.meta && typeof result.meta === "object" ? result.meta : {};
1267
+ const blockingMeta = blockingReview.meta && typeof blockingReview.meta === "object" ? blockingReview.meta : {};
1268
+ if (String(meta.agentId || "").trim() !== String(blockingMeta.agentId || "").trim()) {
1269
+ return "Reviewer override\nThe latest reviewer result does not match the current blocking reviewer.";
1270
+ }
1271
+ const actor = describeDiscordUser(message);
1272
+ await addSharedRoomNote(session.cwd || process.cwd(), "Reviewer outcome overridden by the room", {
1273
+ actorId: actor.userId,
1274
+ actorName: actor.displayName,
1275
+ type: "review-policy-override",
1276
+ meta: { agentId: String(meta.agentId || "").trim() }
1277
+ });
1278
+ return "Reviewer overridden\nExecution may proceed.";
1279
+ }
1280
+ if (normalized === "/switch-executor-to-reviewer" || normalized === "switch executor to reviewer" || normalizedRoomAlias === "switch executor to reviewer") {
1281
+ const sessionId = await ensurePeerSession(runtime, state, message);
1282
+ const { project, session } = await bindSharedRoomForMessage(message, sessionId);
1283
+ if (!project?.enabled) return "This project is not shared.";
1284
+ const reviewer = chooseAgentByRole(project, "reviewer");
1285
+ if (!reviewer) return "Executor switch\nNo reviewer is assigned.";
1286
+ const currentExecutor = chooseAgentByRole(project, "executor");
1287
+ const actor = describeDiscordUser(message);
1288
+ if (currentExecutor?.id && currentExecutor.id !== reviewer.id) {
1289
+ await upsertSharedAgent(session.cwd || process.cwd(), { ...currentExecutor, role: "standby" }, {
1290
+ actorId: actor.userId,
1291
+ actorName: actor.displayName
1292
+ });
1293
+ }
1294
+ await upsertSharedAgent(session.cwd || process.cwd(), { ...reviewer, role: "executor" }, {
1295
+ actorId: actor.userId,
1296
+ actorName: actor.displayName
1297
+ });
1298
+ await addSharedRoomNote(session.cwd || process.cwd(), `${reviewer.ownerName || reviewer.label || reviewer.ownerId || "Reviewer"} is now the executor`, {
1299
+ actorId: actor.userId,
1300
+ actorName: actor.displayName,
1301
+ type: "executor-handoff-reassigned",
1302
+ meta: { currentAgentId: String(reviewer.id || "").trim() }
1303
+ });
1304
+ return `Executor switched\nexecutor: ${reviewer.ownerName || reviewer.label || reviewer.ownerId || reviewer.id || "reviewer"}`;
1053
1305
  }
1054
1306
  if (normalized.startsWith("/assign-role ") || normalizedRoomAlias.startsWith("assign-role ")) {
1055
1307
  const body = normalized.startsWith("/assign-role ") ? value.slice("/assign-role ".length).trim() : roomAlias.slice("assign-role ".length).trim();
@@ -1395,7 +1647,9 @@ async function getLiveBridgeHosts({ cwd = "" } = {}) {
1395
1647
  }
1396
1648
 
1397
1649
  async function runPromptViaBridge(runtime, message, promptText, options = {}) {
1398
- const host = await getLiveBridgeHost();
1650
+ const host = options.targetHost && typeof options.targetHost === "object"
1651
+ ? options.targetHost
1652
+ : await getLiveBridgeHost();
1399
1653
  if (!host) {
1400
1654
  return { error: "No live Waterbrother TUI is connected. Start Waterbrother in the terminal first, then retry." };
1401
1655
  }
@@ -1413,7 +1667,7 @@ async function runPromptViaBridge(runtime, message, promptText, options = {}) {
1413
1667
  displayName: actor.displayName,
1414
1668
  sessionId: String(options.sessionId || "").trim(),
1415
1669
  text: String(promptText || "").trim(),
1416
- requestKind: "prompt",
1670
+ requestKind: String(options.requestKind || "prompt").trim() || "prompt",
1417
1671
  explicitExecution: true,
1418
1672
  targetPid: Number(host?.pid || 0),
1419
1673
  targetSessionId: String(host?.sessionId || "").trim(),