@tritard/waterbrother 0.16.97 → 0.16.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/gateway.js +271 -20
- package/src/shared-project.js +107 -1
package/package.json
CHANGED
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,
|
|
12
|
+
import { acceptSharedInvite, addSharedRoomNote, addSharedTask, addVerificationResult, approveSharedInvite, assignSharedTask, claimSharedOperator, claimSharedTask, commentSharedTask, createSharedInvite, enableSharedProject, getLatestVerificationResult, getSharedTaskHistory, listSharedEvents, listSharedInvites, listSharedTasks, loadSharedProject, moveSharedTask, rejectSharedInvite, releaseSharedOperator, removeSharedMember, setSharedRoom, setSharedRoomMode, setSharedRuntimeProfile, setSharedVerificationMode, upsertSharedAgent, upsertSharedMember } from "./shared-project.js";
|
|
13
13
|
import { buildSelfAwarenessManifest, formatAboutWaterbrother, formatSelfState, resolveLocalConceptQuestion } from "./self-awareness.js";
|
|
14
14
|
|
|
15
15
|
const execFileAsync = promisify(execFile);
|
|
@@ -296,6 +296,7 @@ function buildTelegramRoomOnboardingMarkup({ project = null, actorName = "", exe
|
|
|
296
296
|
const participants = listProjectParticipants(project);
|
|
297
297
|
const agents = listProjectAgents(project);
|
|
298
298
|
const reviewer = chooseReviewerAgent(project);
|
|
299
|
+
const verifier = chooseVerifierAgent(project);
|
|
299
300
|
const blockingReview = getLatestBlockingReviewPolicy(project);
|
|
300
301
|
return [
|
|
301
302
|
"<b>Roundtable room</b>",
|
|
@@ -305,6 +306,7 @@ function buildTelegramRoomOnboardingMarkup({ project = null, actorName = "", exe
|
|
|
305
306
|
`agents: <code>${escapeTelegramHtml(String(agents.length || 0))}</code>`,
|
|
306
307
|
executor?.provider && executor?.model ? `active runtime: <code>${escapeTelegramHtml(`${executor.provider}/${executor.model}`)}</code>` : "",
|
|
307
308
|
reviewer ? `reviewer: <code>${escapeTelegramHtml(reviewer.ownerName || reviewer.label || reviewer.ownerId || reviewer.id || "unknown")}</code>` : "",
|
|
309
|
+
verifier ? `verifier: <code>${escapeTelegramHtml(verifier.ownerName || verifier.label || verifier.ownerId || verifier.id || "unknown")}</code>` : "",
|
|
308
310
|
`blocking review: <code>${escapeTelegramHtml(blockingReview ? "yes" : "no")}</code>`,
|
|
309
311
|
"",
|
|
310
312
|
"<b>What you can do here</b>",
|
|
@@ -312,6 +314,7 @@ function buildTelegramRoomOnboardingMarkup({ project = null, actorName = "", exe
|
|
|
312
314
|
"• ask <code>who is in the room?</code>",
|
|
313
315
|
"• say <code>add Austin as editor</code>",
|
|
314
316
|
"• ask <code>which terminals are live?</code>",
|
|
317
|
+
"• say <code>run verification</code>",
|
|
315
318
|
"• in execute mode, address Waterbrother directly to build or change code",
|
|
316
319
|
"",
|
|
317
320
|
"Use <code>/help</code> for the full command list."
|
|
@@ -336,7 +339,9 @@ function buildTelegramRoomGuidanceMarkup({ project = null, member = null, execut
|
|
|
336
339
|
"• <code>what project is this chat bound to?</code>",
|
|
337
340
|
"• <code>who is in the room?</code>",
|
|
338
341
|
"• <code>who are the bots?</code>",
|
|
339
|
-
role === "owner" ? "• <code>add Austin as editor</code>" : "• <code>what mode are we in?</code>"
|
|
342
|
+
role === "owner" ? "• <code>add Austin as editor</code>" : "• <code>what mode are we in?</code>",
|
|
343
|
+
"• <code>who is the verifier?</code>",
|
|
344
|
+
"• <code>run verification</code>"
|
|
340
345
|
].filter(Boolean).join("\n");
|
|
341
346
|
}
|
|
342
347
|
|
|
@@ -353,6 +358,8 @@ function buildTelegramAgentAnnouncementMarkup(event = {}) {
|
|
|
353
358
|
? "This terminal is the selected executor and can take room work when it is live."
|
|
354
359
|
: role === "reviewer"
|
|
355
360
|
? "This terminal is the room reviewer and can be asked for a second opinion or blocking review."
|
|
361
|
+
: role === "verifier"
|
|
362
|
+
? "This terminal is the verifier and can run tests, checks, and build verification for the room."
|
|
356
363
|
: "This terminal is on standby until the room assigns it a more active role.";
|
|
357
364
|
return [
|
|
358
365
|
`<b>${escapeTelegramHtml(title)}</b>`,
|
|
@@ -363,7 +370,7 @@ function buildTelegramAgentAnnouncementMarkup(event = {}) {
|
|
|
363
370
|
runtime ? `runtime: <code>${escapeTelegramHtml(runtime)}</code>` : "",
|
|
364
371
|
meta.runtimeProfile ? `runtime profile: <code>${escapeTelegramHtml(String(meta.runtimeProfile || ""))}</code>` : "",
|
|
365
372
|
roleGuidance,
|
|
366
|
-
"Examples: <code>which terminals are live?</code>, <code>make this terminal the reviewer</code>, <code>make this terminal the executor</code>"
|
|
373
|
+
"Examples: <code>which terminals are live?</code>, <code>make this terminal the reviewer</code>, <code>make this terminal the verifier</code>, <code>make this terminal the executor</code>"
|
|
367
374
|
].filter(Boolean).join("\n");
|
|
368
375
|
}
|
|
369
376
|
|
|
@@ -502,6 +509,32 @@ function summarizeRuntimeConflict(project, fallbackExecutor = {}) {
|
|
|
502
509
|
};
|
|
503
510
|
}
|
|
504
511
|
|
|
512
|
+
function cleanAgentDisplayValue(value = "") {
|
|
513
|
+
const cleaned = String(value || "").replace(/\bundefined\b/ig, "").trim();
|
|
514
|
+
if (!cleaned) return "";
|
|
515
|
+
if (/^terminal$/i.test(cleaned)) return "";
|
|
516
|
+
return cleaned;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function getAgentOwnerDisplay(agent = {}, fallback = "") {
|
|
520
|
+
const ownerName = cleanAgentDisplayValue(agent?.ownerName);
|
|
521
|
+
const ownerId = cleanAgentDisplayValue(agent?.ownerId);
|
|
522
|
+
const label = cleanAgentDisplayValue(agent?.label);
|
|
523
|
+
const fallbackValue = cleanAgentDisplayValue(fallback);
|
|
524
|
+
return ownerName || ownerId || fallbackValue || label || "unknown";
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function getAgentTerminalDisplay(agent = {}, fallback = "") {
|
|
528
|
+
const label = cleanAgentDisplayValue(agent?.label);
|
|
529
|
+
const fallbackValue = cleanAgentDisplayValue(fallback);
|
|
530
|
+
if (label) return label;
|
|
531
|
+
if (fallbackValue) return fallbackValue;
|
|
532
|
+
const ownerName = cleanAgentDisplayValue(agent?.ownerName);
|
|
533
|
+
if (ownerName) return ownerName + " terminal";
|
|
534
|
+
const ownerId = cleanAgentDisplayValue(agent?.ownerId);
|
|
535
|
+
if (ownerId) return ownerId + " terminal";
|
|
536
|
+
return "current terminal";
|
|
537
|
+
}
|
|
505
538
|
function formatBridgeHostLabel(host = {}) {
|
|
506
539
|
const owner = String(host?.ownerName || host?.ownerId || "").trim();
|
|
507
540
|
const label = String(host?.label || "").trim();
|
|
@@ -526,6 +559,11 @@ function chooseReviewerAgent(project) {
|
|
|
526
559
|
return agents.find((agent) => String(agent?.role || "").trim() === "reviewer") || null;
|
|
527
560
|
}
|
|
528
561
|
|
|
562
|
+
function chooseVerifierAgent(project) {
|
|
563
|
+
const agents = listProjectAgents(project);
|
|
564
|
+
return agents.find((agent) => String(agent?.role || "").trim() === "verifier") || null;
|
|
565
|
+
}
|
|
566
|
+
|
|
529
567
|
function summarizeExecutorReviewerArbitration(project, fallbackExecutor = {}) {
|
|
530
568
|
const executorAgent = chooseExecutorAgent(project, fallbackExecutor);
|
|
531
569
|
const reviewerAgent = chooseReviewerAgent(project);
|
|
@@ -541,6 +579,29 @@ function summarizeExecutorReviewerArbitration(project, fallbackExecutor = {}) {
|
|
|
541
579
|
};
|
|
542
580
|
}
|
|
543
581
|
|
|
582
|
+
function formatVerificationResultMarkup(result = {}, verifier = null) {
|
|
583
|
+
const lines = [
|
|
584
|
+
"<b>Verification result</b>",
|
|
585
|
+
`outcome: <code>${escapeTelegramHtml(String(result.outcome || "failed"))}</code>`
|
|
586
|
+
];
|
|
587
|
+
if (verifier) {
|
|
588
|
+
lines.splice(1, 0, `verifier: <code>${escapeTelegramHtml(verifier.ownerName || verifier.label || verifier.ownerId || verifier.id || "unknown")}</code>`);
|
|
589
|
+
}
|
|
590
|
+
if (Array.isArray(result.commands) && result.commands.length) {
|
|
591
|
+
lines.push("commands:");
|
|
592
|
+
for (const command of result.commands) {
|
|
593
|
+
lines.push(`• <code>${escapeTelegramHtml(command)}</code>`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (result.summary) {
|
|
597
|
+
lines.push("", "<b>Summary</b>", escapeTelegramHtml(result.summary));
|
|
598
|
+
}
|
|
599
|
+
if (Array.isArray(result.logs) && result.logs.length) {
|
|
600
|
+
lines.push("", "<b>Top errors</b>", ...result.logs.slice(0, 6).map((line) => `• ${escapeTelegramHtml(line)}`));
|
|
601
|
+
}
|
|
602
|
+
return lines.filter(Boolean).join("\n");
|
|
603
|
+
}
|
|
604
|
+
|
|
544
605
|
function parseReviewerOutcome(text = "") {
|
|
545
606
|
const value = String(text || "").trim();
|
|
546
607
|
const lower = value.toLowerCase();
|
|
@@ -660,7 +721,11 @@ function formatTelegramProjectMarkup({ cwd, project, chatId = "", title = "" })
|
|
|
660
721
|
}
|
|
661
722
|
|
|
662
723
|
function normalizeTelegramProjectIntentText(text = "") {
|
|
663
|
-
return String(text || "")
|
|
724
|
+
return String(text || "")
|
|
725
|
+
.replace(/[‘’′]/g, "'")
|
|
726
|
+
.replace(/[“”]/g, '"')
|
|
727
|
+
.trim()
|
|
728
|
+
.replace(/\s+/g, " ");
|
|
664
729
|
}
|
|
665
730
|
|
|
666
731
|
function parseTelegramProjectIntent(text = "") {
|
|
@@ -783,15 +848,15 @@ function parseTelegramAgentIntent(text = "") {
|
|
|
783
848
|
}
|
|
784
849
|
}
|
|
785
850
|
|
|
786
|
-
const roleMatch = lowered.match(/\b(executor|reviewer|standby)\b/);
|
|
851
|
+
const roleMatch = lowered.match(/\b(executor|reviewer|verifier|standby)\b/);
|
|
787
852
|
const role = roleMatch?.[1] || "";
|
|
788
853
|
if (!role) return null;
|
|
789
854
|
|
|
790
855
|
const patterns = [
|
|
791
|
-
/^(?:have|make|set)\s+(.+?)['’]s\s+(?:bot|terminal)\s+(?:be|as|to)?\s*(?:the\s+)?(executor|reviewer|standby)\s*$/i,
|
|
792
|
-
/^(?:have|make|set)\s+(.+?)\s+(?:bot|terminal)\s+(?:be|as|to)?\s*(?:the\s+)?(executor|reviewer|standby)\s*$/i,
|
|
793
|
-
/^(.+?)['’]s\s+(?:bot|terminal)\s+should\s+(?:be\s+)?(?:the\s+)?(executor|reviewer|standby)\s*$/i,
|
|
794
|
-
/^(.+?)\s+(?:bot|terminal)\s+should\s+(?:be\s+)?(?:the\s+)?(executor|reviewer|standby)\s*$/i
|
|
856
|
+
/^(?:have|make|set)\s+(.+?)['’]s\s+(?:bot|terminal)\s+(?:be|as|to)?\s*(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i,
|
|
857
|
+
/^(?:have|make|set)\s+(.+?)\s+(?:bot|terminal)\s+(?:be|as|to)?\s*(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i,
|
|
858
|
+
/^(.+?)['’]s\s+(?:bot|terminal)\s+should\s+(?:be\s+)?(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i,
|
|
859
|
+
/^(.+?)\s+(?:bot|terminal)\s+should\s+(?:be\s+)?(?:the\s+)?(executor|reviewer|verifier|standby)\s*$/i
|
|
795
860
|
];
|
|
796
861
|
for (const pattern of patterns) {
|
|
797
862
|
const match = value.match(pattern);
|
|
@@ -834,9 +899,28 @@ function parseTelegramStateIntent(text = "") {
|
|
|
834
899
|
if (/\bwhat can i do here\b/.test(lower) || /\bhow do i use this room\b/.test(lower) || /\bhow do i use this chat\b/.test(lower) || /\bwhat do i do here\b/.test(lower)) {
|
|
835
900
|
return { action: "room-guidance" };
|
|
836
901
|
}
|
|
902
|
+
if (/\bwho(?:'s| is)? the verifier\b/.test(lower) || /\bwhat is the verifier\b/.test(lower) || /\bwho should verify this\b/.test(lower)) {
|
|
903
|
+
return { action: "verifier-status" };
|
|
904
|
+
}
|
|
837
905
|
if (/\bwho are the (?:bots|boys)\b/.test(lower) || /\bwho are the agents\b/.test(lower) || /\bwhat (?:bots|boys) are here\b/.test(lower) || /\bwhat agents are here\b/.test(lower)) {
|
|
838
906
|
return { action: "agent-list" };
|
|
839
907
|
}
|
|
908
|
+
if (/^(?:run|start|do)\s+verification\b/.test(lower) || /^(?:verify)\b/.test(lower) || /\bshow latest verification result\b/.test(lower) || /\bdid verification pass\b/.test(lower)) {
|
|
909
|
+
return { action: /\bshow latest verification result\b/.test(lower) || /\bdid verification pass\b/.test(lower) ? "verification-status" : "run-verification" };
|
|
910
|
+
}
|
|
911
|
+
if (/\bverification mode\b/.test(lower)) {
|
|
912
|
+
const modeMatch = lower.match(/\b(off|manual|auto|blocking)\b/);
|
|
913
|
+
return modeMatch?.[1] ? { action: "verification-mode-set", mode: modeMatch[1] } : { action: "verification-mode-status" };
|
|
914
|
+
}
|
|
915
|
+
if (/\balways verify after execution\b/.test(lower)) {
|
|
916
|
+
return { action: "verification-mode-set", mode: "auto" };
|
|
917
|
+
}
|
|
918
|
+
if (/\bturn off verification\b/.test(lower)) {
|
|
919
|
+
return { action: "verification-mode-set", mode: "off" };
|
|
920
|
+
}
|
|
921
|
+
if (/\bverification should be blocking\b/.test(lower)) {
|
|
922
|
+
return { action: "verification-mode-set", mode: "blocking" };
|
|
923
|
+
}
|
|
840
924
|
if (/\bwhich terminals are live\b/.test(lower) || /\bwhich bots are live\b/.test(lower) || /\bwho is live\b/.test(lower) || /\bwhat terminals are live\b/.test(lower)) {
|
|
841
925
|
return { action: "live-hosts" };
|
|
842
926
|
}
|
|
@@ -980,11 +1064,14 @@ function formatTelegramRoomMarkup(project, options = {}) {
|
|
|
980
1064
|
const liveHosts = Array.isArray(options.liveHosts) ? options.liveHosts : [];
|
|
981
1065
|
const selectedExecutor = chooseExecutorAgent(project, executor);
|
|
982
1066
|
const selectedReviewer = chooseReviewerAgent(project);
|
|
1067
|
+
const selectedVerifier = chooseVerifierAgent(project);
|
|
983
1068
|
const blockingReview = getLatestBlockingReviewPolicy(project);
|
|
984
1069
|
const latestReviewResult = getLatestReviewResult(project);
|
|
985
1070
|
const latestReviewMeta = latestReviewResult?.meta && typeof latestReviewResult.meta === "object" ? latestReviewResult.meta : {};
|
|
1071
|
+
const latestVerificationResult = getLatestVerificationResult(project);
|
|
986
1072
|
const selectedExecutorLiveHost = selectedExecutor ? findLiveHostForAgent(liveHosts, selectedExecutor) : null;
|
|
987
1073
|
const selectedReviewerLiveHost = selectedReviewer ? findLiveHostForAgent(liveHosts, selectedReviewer) : null;
|
|
1074
|
+
const selectedVerifierLiveHost = selectedVerifier ? findLiveHostForAgent(liveHosts, selectedVerifier) : null;
|
|
988
1075
|
const executorBits = [
|
|
989
1076
|
`surface: <code>${escapeTelegramHtml(executor.surface || "telegram")}</code>`,
|
|
990
1077
|
`provider: <code>${escapeTelegramHtml(executor.provider || "unknown")}</code>`,
|
|
@@ -1012,8 +1099,12 @@ function formatTelegramRoomMarkup(project, options = {}) {
|
|
|
1012
1099
|
`executor live: <code>${escapeTelegramHtml(selectedExecutorLiveHost ? "yes" : "no")}</code>`,
|
|
1013
1100
|
`reviewer: <code>${escapeTelegramHtml(selectedReviewer ? (selectedReviewer.ownerName || selectedReviewer.label || selectedReviewer.ownerId || selectedReviewer.id || "none") : "none")}</code>`,
|
|
1014
1101
|
`reviewer live: <code>${escapeTelegramHtml(selectedReviewerLiveHost ? "yes" : "no")}</code>`,
|
|
1102
|
+
`verifier: <code>${escapeTelegramHtml(selectedVerifier ? (selectedVerifier.ownerName || selectedVerifier.label || selectedVerifier.ownerId || selectedVerifier.id || "none") : "none")}</code>`,
|
|
1103
|
+
`verifier live: <code>${escapeTelegramHtml(selectedVerifierLiveHost ? "yes" : "no")}</code>`,
|
|
1015
1104
|
`blocking review: <code>${escapeTelegramHtml(blockingReview ? "yes" : "no")}</code>`,
|
|
1016
1105
|
`latest reviewer outcome: <code>${escapeTelegramHtml(String(latestReviewMeta.outcome || "").trim() || "none")}</code>`,
|
|
1106
|
+
`verification mode: <code>${escapeTelegramHtml(String(project.verificationMode || "manual"))}</code>`,
|
|
1107
|
+
`latest verification outcome: <code>${escapeTelegramHtml(String(latestVerificationResult?.outcome || "").trim() || "none")}</code>`,
|
|
1017
1108
|
"<b>Executor</b>",
|
|
1018
1109
|
...executorBits,
|
|
1019
1110
|
"<b>Runtime Split</b>",
|
|
@@ -1946,7 +2037,31 @@ class TelegramGateway {
|
|
|
1946
2037
|
throw new Error("Only a shared-project owner can change terminal roles.");
|
|
1947
2038
|
}
|
|
1948
2039
|
const actor = this.describeTelegramUser(message?.from || {});
|
|
1949
|
-
const
|
|
2040
|
+
const normalizedTarget = String(intent.target || "").trim().toLowerCase();
|
|
2041
|
+
let targetAgent = resolveProjectAgent(project, intent.target);
|
|
2042
|
+
if (!targetAgent && /^(?:this|my)(?:\s+(?:bot|terminal))?$/.test(normalizedTarget)) {
|
|
2043
|
+
const host = await this.getLiveBridgeHost();
|
|
2044
|
+
if (host) {
|
|
2045
|
+
targetAgent = listProjectAgents(project).find((agent) => (
|
|
2046
|
+
String(agent?.sessionId || "").trim() && String(agent.sessionId || "").trim() === String(host.sessionId || "").trim()
|
|
2047
|
+
) || (
|
|
2048
|
+
String(agent?.ownerId || "").trim() && String(agent.ownerId || "").trim() === String(host.ownerId || "").trim()
|
|
2049
|
+
)) || {
|
|
2050
|
+
id: `agent:telegram-bridge:${String(host.sessionId || host.pid || "current").trim()}`,
|
|
2051
|
+
ownerId: String(host.ownerId || actor.userId || "").trim(),
|
|
2052
|
+
ownerName: getAgentOwnerDisplay(host, actor.displayName || actor.userId || "current operator"),
|
|
2053
|
+
label: getAgentTerminalDisplay(host, actor.displayName ? actor.displayName + " terminal" : (actor.userId ? actor.userId + " terminal" : "current terminal")),
|
|
2054
|
+
surface: String(host.surface || "live-tui").trim(),
|
|
2055
|
+
role: "standby",
|
|
2056
|
+
provider: String(host.provider || "").trim(),
|
|
2057
|
+
model: String(host.model || "").trim(),
|
|
2058
|
+
runtimeProfile: String(host.runtimeProfile || "").trim(),
|
|
2059
|
+
sessionId: String(host.sessionId || "").trim(),
|
|
2060
|
+
cwd: String(session.cwd || this.cwd).trim(),
|
|
2061
|
+
chatId: String(message?.chat?.id || "").trim()
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
1950
2065
|
if (!targetAgent) {
|
|
1951
2066
|
throw new Error(`No terminal found for ${intent.target}. Ask them to connect their Waterbrother bot/terminal first.`);
|
|
1952
2067
|
}
|
|
@@ -1958,26 +2073,30 @@ class TelegramGateway {
|
|
|
1958
2073
|
actorName: actor.displayName || actorId
|
|
1959
2074
|
});
|
|
1960
2075
|
const runtimeLabel = targetAgent.provider && targetAgent.model ? `${targetAgent.provider}/${targetAgent.model}` : "unknown";
|
|
1961
|
-
const actionTitle = intent.action === "agent-review"
|
|
2076
|
+
const actionTitle = intent.action === "agent-review" || intent.role === "reviewer"
|
|
1962
2077
|
? "Roundtable reviewer assigned"
|
|
1963
|
-
: intent.action === "agent-execute"
|
|
2078
|
+
: intent.action === "agent-execute" || intent.role === "executor"
|
|
1964
2079
|
? "Roundtable executor assigned"
|
|
1965
|
-
:
|
|
1966
|
-
|
|
2080
|
+
: intent.role === "verifier"
|
|
2081
|
+
? "Roundtable verifier assigned"
|
|
2082
|
+
: "Roundtable terminal updated";
|
|
2083
|
+
const note = intent.action === "agent-review" || intent.role === "reviewer"
|
|
1967
2084
|
? "This sets the room reviewer role. It does not automatically run a review pass yet."
|
|
1968
|
-
: intent.action === "agent-execute"
|
|
2085
|
+
: intent.action === "agent-execute" || intent.role === "executor"
|
|
1969
2086
|
? "This sets the room executor role. Claim/mode rules still control actual execution."
|
|
1970
|
-
: ""
|
|
1971
|
-
|
|
1972
|
-
|
|
2087
|
+
: intent.role === "verifier"
|
|
2088
|
+
? "This sets the room verifier role. Use run verification when you want Waterbrother to run tests or build checks."
|
|
2089
|
+
: "";
|
|
2090
|
+
const followUp = intent.action === "agent-review" || intent.role === "reviewer"
|
|
2091
|
+
? `Should ${getAgentOwnerDisplay(targetAgent, actor.displayName || actor.userId || "that terminal")} review be advisory or blocking?`
|
|
1973
2092
|
: "";
|
|
1974
2093
|
return {
|
|
1975
2094
|
kind: "agent",
|
|
1976
2095
|
project: nextProject,
|
|
1977
2096
|
markup: [
|
|
1978
2097
|
`<b>${escapeTelegramHtml(actionTitle)}</b>`,
|
|
1979
|
-
`owner: <code>${escapeTelegramHtml(targetAgent
|
|
1980
|
-
`terminal: <code>${escapeTelegramHtml(targetAgent.
|
|
2098
|
+
`owner: <code>${escapeTelegramHtml(getAgentOwnerDisplay(targetAgent, actor.displayName || actor.userId || "current operator"))}</code>`,
|
|
2099
|
+
`terminal: <code>${escapeTelegramHtml(getAgentTerminalDisplay(targetAgent, actor.displayName ? actor.displayName + " terminal" : "current terminal"))}</code>`,
|
|
1981
2100
|
`role: <code>${escapeTelegramHtml(intent.role)}</code>`,
|
|
1982
2101
|
`runtime: <code>${escapeTelegramHtml(runtimeLabel)}</code>`,
|
|
1983
2102
|
`project: <code>${escapeTelegramHtml(nextProject.projectName || path.basename(session.cwd || this.cwd))}</code>`,
|
|
@@ -2245,6 +2364,7 @@ class TelegramGateway {
|
|
|
2245
2364
|
runtimeProfile: host?.runtimeProfile || project?.runtimeProfile || this.channel.defaultRuntimeProfile || this.gateway.defaultRuntimeProfile || "",
|
|
2246
2365
|
hostSessionId: host?.sessionId || ""
|
|
2247
2366
|
};
|
|
2367
|
+
const verifier = chooseVerifierAgent(project);
|
|
2248
2368
|
|
|
2249
2369
|
if (intent.action === "bot-status") {
|
|
2250
2370
|
const lines = [
|
|
@@ -2272,6 +2392,68 @@ class TelegramGateway {
|
|
|
2272
2392
|
].join("\n");
|
|
2273
2393
|
}
|
|
2274
2394
|
|
|
2395
|
+
if (intent.action === "verifier-status") {
|
|
2396
|
+
if (!project?.enabled) {
|
|
2397
|
+
return "This project is not shared.";
|
|
2398
|
+
}
|
|
2399
|
+
if (!verifier) {
|
|
2400
|
+
return [
|
|
2401
|
+
"<b>Verifier</b>",
|
|
2402
|
+
"No verifier is assigned yet.",
|
|
2403
|
+
"Try: <code>make this terminal the verifier</code>"
|
|
2404
|
+
].join("\n");
|
|
2405
|
+
}
|
|
2406
|
+
return [
|
|
2407
|
+
"<b>Verifier</b>",
|
|
2408
|
+
`verifier: <code>${escapeTelegramHtml(getAgentOwnerDisplay(verifier))}</code>`,
|
|
2409
|
+
`role: <code>${escapeTelegramHtml(verifier.role || "verifier")}</code>`,
|
|
2410
|
+
`runtime: <code>${escapeTelegramHtml(verifier.provider && verifier.model ? `${verifier.provider}/${verifier.model}` : "unknown")}</code>`,
|
|
2411
|
+
`verification mode: <code>${escapeTelegramHtml(String(project.verificationMode || "manual"))}</code>`
|
|
2412
|
+
].join("\n");
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
if (intent.action === "verification-mode-status") {
|
|
2416
|
+
if (!project?.enabled) {
|
|
2417
|
+
return "This project is not shared.";
|
|
2418
|
+
}
|
|
2419
|
+
return [
|
|
2420
|
+
"<b>Verification mode</b>",
|
|
2421
|
+
`mode: <code>${escapeTelegramHtml(String(project.verificationMode || "manual"))}</code>`
|
|
2422
|
+
].join("\n");
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
if (intent.action === "verification-mode-set") {
|
|
2426
|
+
if (!project?.enabled) {
|
|
2427
|
+
return "This project is not shared.";
|
|
2428
|
+
}
|
|
2429
|
+
const next = await setSharedVerificationMode(cwd, intent.mode, { actorId, actorName });
|
|
2430
|
+
return [
|
|
2431
|
+
"<b>Verification mode</b>",
|
|
2432
|
+
`mode: <code>${escapeTelegramHtml(String(next.verificationMode || intent.mode || "manual"))}</code>`
|
|
2433
|
+
].join("\n");
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
if (intent.action === "verification-status") {
|
|
2437
|
+
if (!project?.enabled) {
|
|
2438
|
+
return "This project is not shared.";
|
|
2439
|
+
}
|
|
2440
|
+
const result = getLatestVerificationResult(project);
|
|
2441
|
+
if (!result) {
|
|
2442
|
+
return "<b>Verification result</b>\nNo verification result is on record yet.";
|
|
2443
|
+
}
|
|
2444
|
+
return formatVerificationResultMarkup(result, verifier);
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
if (intent.action === "run-verification") {
|
|
2448
|
+
if (!project?.enabled) {
|
|
2449
|
+
return "This project is not shared.";
|
|
2450
|
+
}
|
|
2451
|
+
const result = await this.runLocalVerification(cwd, verifier);
|
|
2452
|
+
const nextProject = await addVerificationResult(cwd, result, { actorId, actorName });
|
|
2453
|
+
const latest = getLatestVerificationResult(nextProject) || result;
|
|
2454
|
+
return formatVerificationResultMarkup(latest, verifier);
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2275
2457
|
if (intent.action === "accept-reviewer-concerns") {
|
|
2276
2458
|
if (!project?.enabled) {
|
|
2277
2459
|
return "This project is not shared.";
|
|
@@ -3146,6 +3328,75 @@ class TelegramGateway {
|
|
|
3146
3328
|
return JSON.parse(stdout);
|
|
3147
3329
|
}
|
|
3148
3330
|
|
|
3331
|
+
async planVerificationCommands(cwd) {
|
|
3332
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
3333
|
+
let scripts = {};
|
|
3334
|
+
try {
|
|
3335
|
+
const raw = await fs.readFile(packageJsonPath, "utf8");
|
|
3336
|
+
const parsed = JSON.parse(raw);
|
|
3337
|
+
scripts = parsed?.scripts && typeof parsed.scripts === "object" ? parsed.scripts : {};
|
|
3338
|
+
} catch {}
|
|
3339
|
+
const commands = [];
|
|
3340
|
+
if (scripts.check) commands.push(["npm", ["run", "check"], "npm run check"]);
|
|
3341
|
+
if (scripts.test) commands.push(["npm", ["test"], "npm test"]);
|
|
3342
|
+
if (scripts.build) commands.push(["npm", ["run", "build"], "npm run build"]);
|
|
3343
|
+
if (!commands.length) {
|
|
3344
|
+
throw new Error("No verification commands are available here. Expected package.json scripts like check, test, or build.");
|
|
3345
|
+
}
|
|
3346
|
+
return commands;
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
async runLocalVerification(cwd, verifier = null) {
|
|
3350
|
+
const planned = await this.planVerificationCommands(cwd);
|
|
3351
|
+
const commands = [];
|
|
3352
|
+
const startedAt = new Date().toISOString();
|
|
3353
|
+
const logs = [];
|
|
3354
|
+
let passedCount = 0;
|
|
3355
|
+
for (const [bin, args, label] of planned) {
|
|
3356
|
+
commands.push(label);
|
|
3357
|
+
try {
|
|
3358
|
+
await execFileAsync(bin, args, {
|
|
3359
|
+
cwd,
|
|
3360
|
+
env: { ...process.env },
|
|
3361
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
3362
|
+
timeout: 10 * 60 * 1000
|
|
3363
|
+
});
|
|
3364
|
+
passedCount += 1;
|
|
3365
|
+
} catch (error) {
|
|
3366
|
+
const combined = `${String(error?.stderr || "")}\n${String(error?.stdout || "")}`.trim();
|
|
3367
|
+
const extracted = combined
|
|
3368
|
+
.split("\n")
|
|
3369
|
+
.map((line) => String(line || "").trim())
|
|
3370
|
+
.filter(Boolean)
|
|
3371
|
+
.slice(0, 6);
|
|
3372
|
+
logs.push(...extracted);
|
|
3373
|
+
const outcome = error?.killed || error?.signal === "SIGTERM" ? "timeout" : (passedCount > 0 ? "partial" : "failed");
|
|
3374
|
+
return {
|
|
3375
|
+
id: "",
|
|
3376
|
+
workItemId: "",
|
|
3377
|
+
verifierAgentId: String(verifier?.id || "").trim(),
|
|
3378
|
+
outcome,
|
|
3379
|
+
summary: `${label} failed${passedCount > 0 ? ` after ${passedCount} passing check${passedCount === 1 ? "" : "s"}` : ""}.`,
|
|
3380
|
+
commands,
|
|
3381
|
+
startedAt,
|
|
3382
|
+
completedAt: new Date().toISOString(),
|
|
3383
|
+
logs
|
|
3384
|
+
};
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
return {
|
|
3388
|
+
id: "",
|
|
3389
|
+
workItemId: "",
|
|
3390
|
+
verifierAgentId: String(verifier?.id || "").trim(),
|
|
3391
|
+
outcome: "passed",
|
|
3392
|
+
summary: `${commands.length} verification command${commands.length === 1 ? "" : "s"} passed.`,
|
|
3393
|
+
commands,
|
|
3394
|
+
startedAt,
|
|
3395
|
+
completedAt: new Date().toISOString(),
|
|
3396
|
+
logs: []
|
|
3397
|
+
};
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3149
3400
|
async startTypingLoop(chatId) {
|
|
3150
3401
|
let stopped = false;
|
|
3151
3402
|
const tick = async () => {
|
package/src/shared-project.js
CHANGED
|
@@ -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
|