@tritard/waterbrother 0.16.141 → 0.16.143

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 +339 -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.143",
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,
@@ -153,6 +155,63 @@ function buildReply(message, botUserId) {
153
155
  return null;
154
156
  }
155
157
 
158
+ function normalizeDiscordIntentText(text = "") {
159
+ return String(text || "")
160
+ .replace(/[‘’]/g, "'")
161
+ .replace(/[“”]/g, '"')
162
+ .replace(/\s+/g, " ")
163
+ .trim();
164
+ }
165
+
166
+ function parseDiscordNaturalRoomIntent(text = "") {
167
+ const value = normalizeDiscordIntentText(text);
168
+ const lower = value.toLowerCase();
169
+ if (!value || lower.startsWith("/")) return null;
170
+
171
+ if (/\bwho(?:'s| is)? in (?:the )?room\b/.test(lower) || /\bwho(?:'s| is)? in (?:the )?project\b/.test(lower) || /\bwho are the members\b/.test(lower) || /\bshow members\b/.test(lower)) {
172
+ return { type: "command", command: "/members" };
173
+ }
174
+ if (/\bwhich terminals are live\b/.test(lower) || /\bwhat terminals are live\b/.test(lower) || /\bwhich bots are live\b/.test(lower) || /\bwho is live\b/.test(lower)) {
175
+ return { type: "command", command: "/terminals" };
176
+ }
177
+ if (/\bwho(?:'s| is)? the verifier\b/.test(lower) || /\bwhat is the verifier\b/.test(lower) || /\bwho should verify this\b/.test(lower)) {
178
+ return { type: "command", command: "/verifier" };
179
+ }
180
+ if (/\bwho(?:'s| is)? the reviewer\b/.test(lower) || /\bwhat is the reviewer\b/.test(lower)) {
181
+ return { type: "command", command: "/reviewer" };
182
+ }
183
+ if (/\bwho(?:'s| is)? the executor\b/.test(lower) || /\bwhat is the executor\b/.test(lower) || /\bwho should take this\b/.test(lower) || /\bwho should handle this\b/.test(lower)) {
184
+ return { type: "command", command: "/executor" };
185
+ }
186
+ if (/\bcompare (?:executor|reviewer)\b/.test(lower) || /\bcompare reviewer and executor\b/.test(lower) || /\bcompare executor and reviewer\b/.test(lower) || /\bdo the reviewer and executor agree\b/.test(lower)) {
187
+ return { type: "compare-executor-reviewer" };
188
+ }
189
+
190
+ const roleMatch = lower.match(/\b(executor|reviewer|verifier|standby)\b/);
191
+ if (roleMatch) {
192
+ const role = String(roleMatch[1] || "").trim().toLowerCase();
193
+ const patterns = [
194
+ /^(?:have|make|set)\s+(.+?)['’]s\s+(?:bot|terminal)\s+(?:be|as|to)?\s*(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i,
195
+ /^(?:have|make|set)\s+(.+?)\s+(?:bot|terminal)\s+(?:be|as|to)?\s*(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i,
196
+ /^(.+?)['’]s\s+(?:bot|terminal)\s+should\s+(?:be\s+)?(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i,
197
+ /^(.+?)\s+(?:bot|terminal)\s+should\s+(?:be\s+)?(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i,
198
+ /^(?:have|make|set)\s+(.+?)\s+(?:be|as|to)?\s*(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i,
199
+ /^(.+?)\s+should\s+(?:be\s+)?(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i
200
+ ];
201
+ for (const pattern of patterns) {
202
+ const match = value.match(pattern);
203
+ if (!match) continue;
204
+ const target = String(match[1] || "").trim();
205
+ const nextRole = String(match[2] || role).trim().toLowerCase();
206
+ if (target && nextRole) {
207
+ return { type: "command", command: `/assign-role ${target} ${nextRole}` };
208
+ }
209
+ }
210
+ }
211
+
212
+ return null;
213
+ }
214
+
156
215
  function buildDiscordHelp() {
157
216
  return [
158
217
  "Waterbrother Discord control",
@@ -170,6 +229,14 @@ function buildDiscordHelp() {
170
229
  "/executor show the selected executor",
171
230
  "/reviewer show the assigned reviewer",
172
231
  "/verifier show the assigned verifier",
232
+ "/verify run verification through the assigned verifier or live terminal",
233
+ "/verification show the latest verification result",
234
+ "/rerun-verification rerun verification",
235
+ "/override-verification override the current blocking verification",
236
+ "/review-status show blocking review and latest reviewer outcome",
237
+ "/accept-reviewer-concerns clear blocking review by accepting the reviewer concerns",
238
+ "/override-reviewer override the current blocking reviewer outcome",
239
+ "/switch-executor-to-reviewer make the reviewer the executor",
173
240
  "/assign-role <terminal|this terminal> <executor|reviewer|verifier|standby> assign a room role",
174
241
  "/members list shared room members",
175
242
  "/tasks list shared room tasks",
@@ -399,6 +466,107 @@ function formatDiscordTaskHistory(result = {}) {
399
466
  return lines.join("\n");
400
467
  }
401
468
 
469
+ function getLatestBlockingReviewPolicy(project = {}) {
470
+ const events = Array.isArray(project?.recentEvents) ? [...project.recentEvents] : [];
471
+ const ordered = events
472
+ .filter((event) => event?.createdAt)
473
+ .sort((a, b) => String(b.createdAt || "").localeCompare(String(a.createdAt || "")));
474
+ const resolvedAgents = new Set();
475
+ for (const event of ordered) {
476
+ const type = String(event?.type || "").trim();
477
+ const meta = event?.meta && typeof event.meta === "object" ? event.meta : {};
478
+ const agentId = String(meta.agentId || "").trim();
479
+ if (!agentId) continue;
480
+ if (type === "review-policy-cleared" || type === "review-policy-override") {
481
+ resolvedAgents.add(agentId);
482
+ continue;
483
+ }
484
+ if (type === "review-policy" && String(meta.policy || "").trim() === "blocking" && !resolvedAgents.has(agentId)) {
485
+ return event;
486
+ }
487
+ }
488
+ return null;
489
+ }
490
+
491
+ function getLatestReviewResult(project = {}) {
492
+ const events = Array.isArray(project?.recentEvents) ? [...project.recentEvents] : [];
493
+ const ordered = events
494
+ .filter((event) => event?.createdAt)
495
+ .sort((a, b) => String(b.createdAt || "").localeCompare(String(a.createdAt || "")));
496
+ return ordered.find((event) => String(event?.type || "").trim() === "review-result") || null;
497
+ }
498
+
499
+ function getBlockingVerificationResult(project = {}) {
500
+ if (String(project?.verificationMode || "manual").trim() !== "blocking") {
501
+ return null;
502
+ }
503
+ const latestResult = getLatestVerificationResult(project);
504
+ const latestId = String(latestResult?.id || "").trim();
505
+ const latestOutcome = String(latestResult?.outcome || "").trim().toLowerCase();
506
+ if (!latestId || !latestOutcome || latestOutcome === "passed") return null;
507
+ const events = Array.isArray(project?.recentEvents) ? [...project.recentEvents] : [];
508
+ const ordered = events
509
+ .filter((event) => event?.createdAt)
510
+ .sort((a, b) => String(b.createdAt || "").localeCompare(String(a.createdAt || "")));
511
+ for (const event of ordered) {
512
+ const type = String(event?.type || "").trim();
513
+ const meta = event?.meta && typeof event.meta === "object" ? event.meta : {};
514
+ if (String(meta.verificationResultId || "").trim() !== latestId) continue;
515
+ if (type === "verification-override") return null;
516
+ if (type === "verification-result") return latestResult;
517
+ }
518
+ return latestResult;
519
+ }
520
+
521
+ function formatDiscordVerificationResult(result = {}, verifier = null) {
522
+ const lines = [
523
+ "Verification result",
524
+ `outcome: ${String(result.outcome || "failed")}`
525
+ ];
526
+ if (verifier) lines.splice(1, 0, `verifier: ${getAgentOwnerDisplay(verifier)}`);
527
+ if (result.failedCommand) lines.push(`failed step: ${String(result.failedCommand || "")}`);
528
+ if (result.isolation) lines.push(`environment: ${String(result.isolation || "")}`);
529
+ if (Array.isArray(result.commands) && result.commands.length) {
530
+ lines.push("commands:");
531
+ for (const command of result.commands) lines.push(`- ${command}`);
532
+ }
533
+ if (result.summary) lines.push("", "Summary", String(result.summary || ""));
534
+ if (Array.isArray(result.logs) && result.logs.length) {
535
+ lines.push("", "Top errors", ...result.logs.slice(0, 6).map((line) => `- ${line}`));
536
+ }
537
+ if (String(result.outcome || "").trim() !== "passed") {
538
+ lines.push("", "Next actions", "- /rerun-verification", "- /override-verification");
539
+ }
540
+ return lines.join("\n");
541
+ }
542
+
543
+ function formatDiscordReviewStatus(project = {}) {
544
+ const blockingReview = getLatestBlockingReviewPolicy(project);
545
+ const latestReview = getLatestReviewResult(project);
546
+ const reviewMeta = latestReview?.meta && typeof latestReview.meta === "object" ? latestReview.meta : {};
547
+ return [
548
+ "Reviewer status",
549
+ `blocking review: ${blockingReview ? "yes" : "no"}`,
550
+ `latest reviewer outcome: ${String(reviewMeta.outcome || "").trim() || "none"}`,
551
+ reviewMeta.agentId ? `reviewer agent: ${reviewMeta.agentId}` : ""
552
+ ].filter(Boolean).join("\n");
553
+ }
554
+
555
+ async function formatDiscordExecutorReviewerCompare(project = {}, session = null) {
556
+ const executor = chooseAgentByRole(project, "executor");
557
+ const reviewer = chooseAgentByRole(project, "reviewer");
558
+ const liveHosts = await getLiveBridgeHosts({ cwd: session?.cwd || "" });
559
+ const executorLive = executor ? liveHosts.some((host) => String(host?.ownerId || "") === String(executor?.ownerId || "") || String(host?.sessionId || "") === String(executor?.sessionId || "")) : false;
560
+ const reviewerLive = reviewer ? liveHosts.some((host) => String(host?.ownerId || "") === String(reviewer?.ownerId || "") || String(host?.sessionId || "") === String(reviewer?.sessionId || "")) : false;
561
+ return [
562
+ formatDiscordAgentStatus("Executor", executor, { live: executorLive }),
563
+ "",
564
+ formatDiscordAgentStatus("Reviewer", reviewer, { live: reviewerLive }),
565
+ "",
566
+ formatDiscordReviewStatus(project)
567
+ ].join("\n");
568
+ }
569
+
402
570
  function parseDiscordInviteCommand(message = {}, text = "") {
403
571
  const value = String(text || "").trim();
404
572
  const match = value.match(/^\/?(?:room\s+)?invite\s+(.+)$/i);
@@ -988,6 +1156,10 @@ async function handleDiscordControlCommand(runtime, state, message, rawText) {
988
1156
  const value = String(rawText || "").replace(/\s+/g, " ").trim();
989
1157
  const normalized = value.toLowerCase();
990
1158
  if (!normalized) return null;
1159
+ const naturalIntent = parseDiscordNaturalRoomIntent(value);
1160
+ if (naturalIntent?.type === "command" && naturalIntent.command) {
1161
+ return handleDiscordControlCommand(runtime, state, message, naturalIntent.command);
1162
+ }
991
1163
  const roomAlias = normalized.startsWith("/room ") ? value.slice("/room ".length).trim() : (normalized.startsWith("room ") ? value.slice("room ".length).trim() : "");
992
1164
  const normalizedRoomAlias = roomAlias.toLowerCase();
993
1165
  if (normalized === "/help" || normalized === "help") {
@@ -1049,7 +1221,169 @@ async function handleDiscordControlCommand(runtime, state, message, rawText) {
1049
1221
  const agent = chooseAgentByRole(project, "verifier");
1050
1222
  const liveHosts = await getLiveBridgeHosts({ cwd: session?.cwd || "" });
1051
1223
  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 });
1224
+ const latestVerification = getLatestVerificationResult(project);
1225
+ const blockingVerification = getBlockingVerificationResult(project);
1226
+ return [
1227
+ formatDiscordAgentStatus("Verifier", agent, { live }),
1228
+ `verification mode: ${String(project.verificationMode || "manual")}`,
1229
+ `blocking verification: ${blockingVerification ? "yes" : "no"}`,
1230
+ `latest verification outcome: ${String(latestVerification?.outcome || "").trim() || "none"}`
1231
+ ].filter(Boolean).join("\n");
1232
+ }
1233
+ if (normalized === "/verification" || normalized === "verification" || normalizedRoomAlias === "verification") {
1234
+ const sessionId = await ensurePeerSession(runtime, state, message);
1235
+ const { project } = await bindSharedRoomForMessage(message, sessionId);
1236
+ if (!project?.enabled) return "This project is not shared.";
1237
+ const verifier = chooseAgentByRole(project, "verifier");
1238
+ const result = getLatestVerificationResult(project);
1239
+ if (!result) return "Verification result\nNo verification result is on record yet.";
1240
+ return formatDiscordVerificationResult(result, verifier);
1241
+ }
1242
+ if (normalized === "/verify" || normalized === "verify" || normalized === "/run-verification" || normalizedRoomAlias === "verify" || normalizedRoomAlias === "run-verification") {
1243
+ const sessionId = await ensurePeerSession(runtime, state, message);
1244
+ const { project, session } = await bindSharedRoomForMessage(message, sessionId);
1245
+ if (!project?.enabled) return "This project is not shared.";
1246
+ const actor = describeDiscordUser(message);
1247
+ const verifier = chooseAgentByRole(project, "verifier");
1248
+ const liveHosts = await getLiveBridgeHosts({ cwd: session?.cwd || "" });
1249
+ const verifierHost = verifier ? liveHosts.find((host) => String(host?.ownerId || "") === String(verifier?.ownerId || "") || String(host?.sessionId || "") === String(verifier?.sessionId || "")) : null;
1250
+ const targetHost = verifierHost || await getLiveBridgeHost({ cwd: session?.cwd || "" });
1251
+ if (!targetHost) {
1252
+ return "No live Waterbrother TUI is connected. Start Waterbrother in the terminal first, then retry.";
1253
+ }
1254
+ const bridged = await runPromptViaBridge(runtime, message, "run verification", {
1255
+ sessionId,
1256
+ requestKind: "verification",
1257
+ targetHost
1258
+ });
1259
+ const routedResult = bridged?.meta?.verificationResult && typeof bridged.meta.verificationResult === "object"
1260
+ ? bridged.meta.verificationResult
1261
+ : null;
1262
+ if (routedResult) {
1263
+ const nextProject = await addVerificationResult(session.cwd || process.cwd(), routedResult, {
1264
+ actorId: actor.userId,
1265
+ actorName: actor.displayName
1266
+ });
1267
+ const latest = getLatestVerificationResult(nextProject) || routedResult;
1268
+ return formatDiscordVerificationResult(latest, verifier);
1269
+ }
1270
+ if (bridged?.error) return bridged.error;
1271
+ return String(bridged?.content || "").trim() || "Verification result\n(no content)";
1272
+ }
1273
+ if (normalized === "/rerun-verification" || normalized === "rerun verification" || normalizedRoomAlias === "rerun verification") {
1274
+ return handleDiscordControlCommand(runtime, state, message, "/verify");
1275
+ }
1276
+ if (normalized === "/override-verification" || normalized === "override verification" || normalizedRoomAlias === "override verification") {
1277
+ const sessionId = await ensurePeerSession(runtime, state, message);
1278
+ const { project, session } = await bindSharedRoomForMessage(message, sessionId);
1279
+ if (!project?.enabled) return "This project is not shared.";
1280
+ const blockingVerification = getBlockingVerificationResult(project);
1281
+ if (!blockingVerification) return "Verification override\nNo blocking verification is active.";
1282
+ const actor = describeDiscordUser(message);
1283
+ await addSharedRoomNote(session.cwd || process.cwd(), "Blocking verification overridden for now", {
1284
+ actorId: actor.userId,
1285
+ actorName: actor.displayName,
1286
+ type: "verification-override",
1287
+ meta: {
1288
+ verificationResultId: String(blockingVerification.id || "").trim(),
1289
+ outcome: String(blockingVerification.outcome || "").trim()
1290
+ }
1291
+ });
1292
+ return [
1293
+ "Verification overridden",
1294
+ `latest outcome: ${String(blockingVerification.outcome || "failed")}`,
1295
+ "Execution can proceed for now."
1296
+ ].join("\n");
1297
+ }
1298
+ if (normalized === "/review-status" || normalized === "review status" || normalizedRoomAlias === "review-status" || normalizedRoomAlias === "review status") {
1299
+ const sessionId = await ensurePeerSession(runtime, state, message);
1300
+ const { project } = await bindSharedRoomForMessage(message, sessionId);
1301
+ if (!project?.enabled) return "This project is not shared.";
1302
+ return formatDiscordReviewStatus(project);
1303
+ }
1304
+ if (naturalIntent?.type === "compare-executor-reviewer") {
1305
+ const sessionId = await ensurePeerSession(runtime, state, message);
1306
+ const { project, session } = await bindSharedRoomForMessage(message, sessionId);
1307
+ if (!project?.enabled) return "This project is not shared.";
1308
+ return formatDiscordExecutorReviewerCompare(project, session);
1309
+ }
1310
+ if (normalized === "/accept-reviewer-concerns" || normalized === "accept reviewer concerns" || normalizedRoomAlias === "accept reviewer concerns") {
1311
+ const sessionId = await ensurePeerSession(runtime, state, message);
1312
+ const { project, session } = await bindSharedRoomForMessage(message, sessionId);
1313
+ if (!project?.enabled) return "This project is not shared.";
1314
+ const blockingReview = getLatestBlockingReviewPolicy(project);
1315
+ if (!blockingReview) return "Reviewer concerns\nNo blocking review is active.";
1316
+ const result = getLatestReviewResult(project);
1317
+ if (!result) return "Reviewer concerns\nNo reviewer result is on record yet.";
1318
+ const meta = result.meta && typeof result.meta === "object" ? result.meta : {};
1319
+ const blockingMeta = blockingReview.meta && typeof blockingReview.meta === "object" ? blockingReview.meta : {};
1320
+ if (String(meta.agentId || "").trim() !== String(blockingMeta.agentId || "").trim()) {
1321
+ return "Reviewer concerns\nThe latest reviewer result does not match the current blocking reviewer.";
1322
+ }
1323
+ if (String(meta.outcome || "").trim() !== "concerns") {
1324
+ return "Reviewer concerns\nThe latest reviewer result is not concerns.";
1325
+ }
1326
+ const actor = describeDiscordUser(message);
1327
+ await addSharedRoomNote(session.cwd || process.cwd(), "Reviewer concerns accepted; revise before continuing", {
1328
+ actorId: actor.userId,
1329
+ actorName: actor.displayName,
1330
+ type: "review-concerns-accepted",
1331
+ meta: { agentId: String(meta.agentId || "").trim() }
1332
+ });
1333
+ await addSharedRoomNote(session.cwd || process.cwd(), "Blocking review cleared after reviewer concerns were accepted", {
1334
+ actorId: actor.userId,
1335
+ actorName: actor.displayName,
1336
+ type: "review-policy-cleared",
1337
+ meta: { agentId: String(meta.agentId || "").trim() }
1338
+ });
1339
+ return "Reviewer concerns accepted\nBlocking review has been cleared so the team can revise and continue.";
1340
+ }
1341
+ if (normalized === "/override-reviewer" || normalized === "override reviewer" || normalizedRoomAlias === "override reviewer") {
1342
+ const sessionId = await ensurePeerSession(runtime, state, message);
1343
+ const { project, session } = await bindSharedRoomForMessage(message, sessionId);
1344
+ if (!project?.enabled) return "This project is not shared.";
1345
+ const blockingReview = getLatestBlockingReviewPolicy(project);
1346
+ if (!blockingReview) return "Reviewer override\nNo blocking review is active.";
1347
+ const result = getLatestReviewResult(project);
1348
+ const meta = result?.meta && typeof result.meta === "object" ? result.meta : {};
1349
+ const blockingMeta = blockingReview.meta && typeof blockingReview.meta === "object" ? blockingReview.meta : {};
1350
+ if (String(meta.agentId || "").trim() !== String(blockingMeta.agentId || "").trim()) {
1351
+ return "Reviewer override\nThe latest reviewer result does not match the current blocking reviewer.";
1352
+ }
1353
+ const actor = describeDiscordUser(message);
1354
+ await addSharedRoomNote(session.cwd || process.cwd(), "Reviewer outcome overridden by the room", {
1355
+ actorId: actor.userId,
1356
+ actorName: actor.displayName,
1357
+ type: "review-policy-override",
1358
+ meta: { agentId: String(meta.agentId || "").trim() }
1359
+ });
1360
+ return "Reviewer overridden\nExecution may proceed.";
1361
+ }
1362
+ if (normalized === "/switch-executor-to-reviewer" || normalized === "switch executor to reviewer" || normalizedRoomAlias === "switch executor to reviewer") {
1363
+ const sessionId = await ensurePeerSession(runtime, state, message);
1364
+ const { project, session } = await bindSharedRoomForMessage(message, sessionId);
1365
+ if (!project?.enabled) return "This project is not shared.";
1366
+ const reviewer = chooseAgentByRole(project, "reviewer");
1367
+ if (!reviewer) return "Executor switch\nNo reviewer is assigned.";
1368
+ const currentExecutor = chooseAgentByRole(project, "executor");
1369
+ const actor = describeDiscordUser(message);
1370
+ if (currentExecutor?.id && currentExecutor.id !== reviewer.id) {
1371
+ await upsertSharedAgent(session.cwd || process.cwd(), { ...currentExecutor, role: "standby" }, {
1372
+ actorId: actor.userId,
1373
+ actorName: actor.displayName
1374
+ });
1375
+ }
1376
+ await upsertSharedAgent(session.cwd || process.cwd(), { ...reviewer, role: "executor" }, {
1377
+ actorId: actor.userId,
1378
+ actorName: actor.displayName
1379
+ });
1380
+ await addSharedRoomNote(session.cwd || process.cwd(), `${reviewer.ownerName || reviewer.label || reviewer.ownerId || "Reviewer"} is now the executor`, {
1381
+ actorId: actor.userId,
1382
+ actorName: actor.displayName,
1383
+ type: "executor-handoff-reassigned",
1384
+ meta: { currentAgentId: String(reviewer.id || "").trim() }
1385
+ });
1386
+ return `Executor switched\nexecutor: ${reviewer.ownerName || reviewer.label || reviewer.ownerId || reviewer.id || "reviewer"}`;
1053
1387
  }
1054
1388
  if (normalized.startsWith("/assign-role ") || normalizedRoomAlias.startsWith("assign-role ")) {
1055
1389
  const body = normalized.startsWith("/assign-role ") ? value.slice("/assign-role ".length).trim() : roomAlias.slice("assign-role ".length).trim();
@@ -1395,7 +1729,9 @@ async function getLiveBridgeHosts({ cwd = "" } = {}) {
1395
1729
  }
1396
1730
 
1397
1731
  async function runPromptViaBridge(runtime, message, promptText, options = {}) {
1398
- const host = await getLiveBridgeHost();
1732
+ const host = options.targetHost && typeof options.targetHost === "object"
1733
+ ? options.targetHost
1734
+ : await getLiveBridgeHost();
1399
1735
  if (!host) {
1400
1736
  return { error: "No live Waterbrother TUI is connected. Start Waterbrother in the terminal first, then retry." };
1401
1737
  }
@@ -1413,7 +1749,7 @@ async function runPromptViaBridge(runtime, message, promptText, options = {}) {
1413
1749
  displayName: actor.displayName,
1414
1750
  sessionId: String(options.sessionId || "").trim(),
1415
1751
  text: String(promptText || "").trim(),
1416
- requestKind: "prompt",
1752
+ requestKind: String(options.requestKind || "prompt").trim() || "prompt",
1417
1753
  explicitExecution: true,
1418
1754
  targetPid: Number(host?.pid || 0),
1419
1755
  targetSessionId: String(host?.sessionId || "").trim(),