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