@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.
- package/package.json +1 -1
- package/src/discord.js +257 -3
package/package.json
CHANGED
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
|
-
|
|
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 =
|
|
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(),
|