@tritard/waterbrother 0.16.73 → 0.16.75
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-state.js +1 -0
- package/src/gateway.js +190 -6
- package/src/shared-project.js +16 -0
package/package.json
CHANGED
package/src/gateway-state.js
CHANGED
|
@@ -35,6 +35,7 @@ function normalizeGatewayState(parsed = {}) {
|
|
|
35
35
|
lastPrompt: String(item?.lastPrompt || "").trim(),
|
|
36
36
|
kind: String(item?.kind || "follow-up").trim() || "follow-up",
|
|
37
37
|
source: String(item?.source || "assistant-question").trim() || "assistant-question",
|
|
38
|
+
context: item?.context && typeof item.context === "object" ? { ...item.context } : {},
|
|
38
39
|
expiresAt: String(item?.expiresAt || "").trim()
|
|
39
40
|
}
|
|
40
41
|
])
|
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, addSharedTask, approveSharedInvite, assignSharedTask, claimSharedOperator, claimSharedTask, commentSharedTask, createSharedInvite, enableSharedProject, getSharedTaskHistory, listSharedEvents, listSharedInvites, listSharedTasks, loadSharedProject, moveSharedTask, rejectSharedInvite, releaseSharedOperator, setSharedRoom, setSharedRoomMode, setSharedRuntimeProfile, removeSharedMember, upsertSharedAgent, upsertSharedMember } from "./shared-project.js";
|
|
12
|
+
import { acceptSharedInvite, addSharedRoomNote, addSharedTask, approveSharedInvite, assignSharedTask, claimSharedOperator, claimSharedTask, commentSharedTask, createSharedInvite, enableSharedProject, getSharedTaskHistory, listSharedEvents, listSharedInvites, listSharedTasks, loadSharedProject, moveSharedTask, rejectSharedInvite, releaseSharedOperator, setSharedRoom, setSharedRoomMode, setSharedRuntimeProfile, removeSharedMember, 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);
|
|
@@ -419,6 +419,28 @@ function chooseExecutorAgent(project, fallbackExecutor = {}) {
|
|
|
419
419
|
return null;
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
+
function getLatestBlockingReviewPolicy(project) {
|
|
423
|
+
const events = Array.isArray(project?.recentEvents) ? [...project.recentEvents] : [];
|
|
424
|
+
const ordered = events
|
|
425
|
+
.filter((event) => event?.createdAt)
|
|
426
|
+
.sort((a, b) => String(b.createdAt || "").localeCompare(String(a.createdAt || "")));
|
|
427
|
+
const resolvedAgents = new Set();
|
|
428
|
+
for (const event of ordered) {
|
|
429
|
+
const type = String(event?.type || "").trim();
|
|
430
|
+
const meta = event?.meta && typeof event.meta === "object" ? event.meta : {};
|
|
431
|
+
const agentId = String(meta.agentId || "").trim();
|
|
432
|
+
if (!agentId) continue;
|
|
433
|
+
if (type === "review-policy-cleared" || type === "review-policy-override") {
|
|
434
|
+
resolvedAgents.add(agentId);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (type === "review-policy" && String(meta.policy || "").trim() === "blocking" && !resolvedAgents.has(agentId)) {
|
|
438
|
+
return event;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
|
|
422
444
|
function memberRoleWeight(role = "") {
|
|
423
445
|
const normalized = String(role || "").trim().toLowerCase();
|
|
424
446
|
if (normalized === "owner") return 3;
|
|
@@ -1293,9 +1315,15 @@ class TelegramGateway {
|
|
|
1293
1315
|
}
|
|
1294
1316
|
|
|
1295
1317
|
async rememberContinuation(message, text = "") {
|
|
1318
|
+
return this.rememberContinuationWithOptions(message, text);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
async rememberContinuationWithOptions(message, text = "", extra = {}) {
|
|
1296
1322
|
const body = String(text || "").trim();
|
|
1297
1323
|
if (!body) return;
|
|
1298
|
-
const asksFollowUp =
|
|
1324
|
+
const asksFollowUp = extra.force === true
|
|
1325
|
+
? true
|
|
1326
|
+
:
|
|
1299
1327
|
/\?\s*$/.test(body)
|
|
1300
1328
|
|| /\b(would you like|do you want|which\b|what\b.*\?|how\b.*\?|who\b.*\?|where\b.*\?)\b/i.test(body);
|
|
1301
1329
|
if (!asksFollowUp) return;
|
|
@@ -1304,8 +1332,9 @@ class TelegramGateway {
|
|
|
1304
1332
|
chatId: String(message?.chat?.id || "").trim(),
|
|
1305
1333
|
userId: String(message?.from?.id || "").trim(),
|
|
1306
1334
|
lastPrompt: body.slice(0, 400),
|
|
1307
|
-
kind: "follow-up",
|
|
1308
|
-
source: "assistant-question",
|
|
1335
|
+
kind: String(extra.kind || "follow-up").trim() || "follow-up",
|
|
1336
|
+
source: String(extra.source || "assistant-question").trim() || "assistant-question",
|
|
1337
|
+
context: extra.context && typeof extra.context === "object" ? { ...extra.context } : {},
|
|
1309
1338
|
expiresAt: new Date(Date.now() + TELEGRAM_CONTINUATION_TTL_MS).toISOString()
|
|
1310
1339
|
};
|
|
1311
1340
|
await this.persistState();
|
|
@@ -1698,6 +1727,9 @@ class TelegramGateway {
|
|
|
1698
1727
|
: intent.action === "agent-execute"
|
|
1699
1728
|
? "This sets the room executor role. Claim/mode rules still control actual execution."
|
|
1700
1729
|
: "";
|
|
1730
|
+
const followUp = intent.action === "agent-review"
|
|
1731
|
+
? `Should ${targetAgent.ownerName || targetAgent.label || "that terminal"} review be advisory or blocking?`
|
|
1732
|
+
: "";
|
|
1701
1733
|
return {
|
|
1702
1734
|
kind: "agent",
|
|
1703
1735
|
project: nextProject,
|
|
@@ -1708,11 +1740,119 @@ class TelegramGateway {
|
|
|
1708
1740
|
`role: <code>${escapeTelegramHtml(intent.role)}</code>`,
|
|
1709
1741
|
`runtime: <code>${escapeTelegramHtml(runtimeLabel)}</code>`,
|
|
1710
1742
|
`project: <code>${escapeTelegramHtml(nextProject.projectName || path.basename(session.cwd || this.cwd))}</code>`,
|
|
1711
|
-
note
|
|
1712
|
-
|
|
1743
|
+
note,
|
|
1744
|
+
followUp
|
|
1745
|
+
].filter(Boolean).join("\n"),
|
|
1746
|
+
continuation: followUp
|
|
1747
|
+
? {
|
|
1748
|
+
text: followUp,
|
|
1749
|
+
kind: "agent-review-policy",
|
|
1750
|
+
source: "agent-delegation",
|
|
1751
|
+
context: {
|
|
1752
|
+
agentId: String(targetAgent.id || "").trim(),
|
|
1753
|
+
ownerName: String(targetAgent.ownerName || targetAgent.label || "").trim(),
|
|
1754
|
+
projectName: String(nextProject.projectName || "").trim()
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
: null
|
|
1713
1758
|
};
|
|
1714
1759
|
}
|
|
1715
1760
|
|
|
1761
|
+
async handleStructuredContinuation(message, sessionId, continuation, peer) {
|
|
1762
|
+
if (!continuation?.kind) return null;
|
|
1763
|
+
const reply = this.stripBotMention(String(message?.text || "").trim()).toLowerCase();
|
|
1764
|
+
if (continuation.kind === "agent-review-policy") {
|
|
1765
|
+
const policy = /\b(blocking|block|required|gate)\b/.test(reply)
|
|
1766
|
+
? "blocking"
|
|
1767
|
+
: /\b(advisory|advice|optional|non-blocking|nonblocking)\b/.test(reply)
|
|
1768
|
+
? "advisory"
|
|
1769
|
+
: "";
|
|
1770
|
+
if (!policy) {
|
|
1771
|
+
return {
|
|
1772
|
+
markup: `Reply with <code>advisory</code> or <code>blocking</code> for ${escapeTelegramHtml(continuation.context?.ownerName || "that review")}.`,
|
|
1773
|
+
remember: {
|
|
1774
|
+
text: String(continuation.lastPrompt || "").trim() || "Should this review be advisory or blocking?",
|
|
1775
|
+
kind: continuation.kind,
|
|
1776
|
+
source: continuation.source,
|
|
1777
|
+
context: continuation.context || {},
|
|
1778
|
+
force: true
|
|
1779
|
+
}
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
const session = await loadSession(sessionId);
|
|
1783
|
+
const actorId = String(message?.from?.id || "").trim();
|
|
1784
|
+
const actorName = peer?.username || [message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim() || actorId;
|
|
1785
|
+
await addSharedRoomNote(session.cwd || this.cwd, `${continuation.context?.ownerName || "Assigned"} review should be ${policy}`, {
|
|
1786
|
+
actorId,
|
|
1787
|
+
actorName,
|
|
1788
|
+
type: "review-policy",
|
|
1789
|
+
meta: {
|
|
1790
|
+
agentId: String(continuation.context?.agentId || "").trim(),
|
|
1791
|
+
policy
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
return {
|
|
1795
|
+
markup: [
|
|
1796
|
+
"<b>Review policy noted</b>",
|
|
1797
|
+
`reviewer: <code>${escapeTelegramHtml(continuation.context?.ownerName || "unknown")}</code>`,
|
|
1798
|
+
`policy: <code>${escapeTelegramHtml(policy)}</code>`,
|
|
1799
|
+
"This is recorded in the room as coordination guidance. Automatic enforcement is not wired yet."
|
|
1800
|
+
].join("\n")
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
if (continuation.kind === "blocking-review-override") {
|
|
1804
|
+
const reply = this.stripBotMention(String(message?.text || "").trim()).toLowerCase();
|
|
1805
|
+
const actorId = String(message?.from?.id || "").trim();
|
|
1806
|
+
const actorName = peer?.username || [message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim() || actorId;
|
|
1807
|
+
const session = await loadSession(sessionId);
|
|
1808
|
+
if (/\boverride\b/.test(reply)) {
|
|
1809
|
+
await addSharedRoomNote(session.cwd || this.cwd, `${continuation.context?.ownerName || "Assigned"} blocking review overridden for now`, {
|
|
1810
|
+
actorId,
|
|
1811
|
+
actorName,
|
|
1812
|
+
type: "review-policy-override",
|
|
1813
|
+
meta: {
|
|
1814
|
+
agentId: String(continuation.context?.agentId || "").trim()
|
|
1815
|
+
}
|
|
1816
|
+
});
|
|
1817
|
+
return {
|
|
1818
|
+
markup: [
|
|
1819
|
+
"<b>Blocking review overridden</b>",
|
|
1820
|
+
`reviewer: <code>${escapeTelegramHtml(continuation.context?.ownerName || "unknown")}</code>`,
|
|
1821
|
+
"Execution can proceed for now."
|
|
1822
|
+
].join("\n")
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
if (/\breviewed\b/.test(reply) || /\bdone\b/.test(reply) || /\bcomplete\b/.test(reply)) {
|
|
1826
|
+
await addSharedRoomNote(session.cwd || this.cwd, `${continuation.context?.ownerName || "Assigned"} blocking review marked complete`, {
|
|
1827
|
+
actorId,
|
|
1828
|
+
actorName,
|
|
1829
|
+
type: "review-policy-cleared",
|
|
1830
|
+
meta: {
|
|
1831
|
+
agentId: String(continuation.context?.agentId || "").trim()
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
return {
|
|
1835
|
+
markup: [
|
|
1836
|
+
"<b>Blocking review cleared</b>",
|
|
1837
|
+
`reviewer: <code>${escapeTelegramHtml(continuation.context?.ownerName || "unknown")}</code>`,
|
|
1838
|
+
"Execution can proceed."
|
|
1839
|
+
].join("\n")
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
return {
|
|
1843
|
+
markup: "Reply with <code>override</code> to proceed anyway, or <code>reviewed</code> after the blocking review is complete.",
|
|
1844
|
+
remember: {
|
|
1845
|
+
text: String(continuation.lastPrompt || "").trim() || "Reply with override or reviewed.",
|
|
1846
|
+
kind: continuation.kind,
|
|
1847
|
+
source: continuation.source,
|
|
1848
|
+
context: continuation.context || {},
|
|
1849
|
+
force: true
|
|
1850
|
+
}
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
return null;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1716
1856
|
async handleStateIntent(message, sessionId, intent) {
|
|
1717
1857
|
const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
1718
1858
|
const cwd = session.cwd || this.cwd;
|
|
@@ -3195,6 +3335,21 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
|
|
|
3195
3335
|
const isExplicitRun = text.startsWith("/run ");
|
|
3196
3336
|
const rawPromptText = this.stripBotMention(isExplicitRun ? text.replace("/run", "").trim() : text);
|
|
3197
3337
|
const promptText = continuation ? buildContinuationPrompt(rawPromptText, continuation) : rawPromptText;
|
|
3338
|
+
if (continuation?.kind && continuation.kind !== "follow-up") {
|
|
3339
|
+
try {
|
|
3340
|
+
const structured = await this.handleStructuredContinuation(message, sessionId, continuation, peer);
|
|
3341
|
+
if (structured?.markup) {
|
|
3342
|
+
await this.sendMarkup(message.chat.id, structured.markup, message.message_id);
|
|
3343
|
+
if (structured.remember) {
|
|
3344
|
+
await this.rememberContinuationWithOptions(message, structured.remember.text, structured.remember);
|
|
3345
|
+
}
|
|
3346
|
+
return;
|
|
3347
|
+
}
|
|
3348
|
+
} catch (error) {
|
|
3349
|
+
await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
|
|
3350
|
+
return;
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3198
3353
|
const groupIntent = this.isGroupChat(message)
|
|
3199
3354
|
? (isExplicitRun
|
|
3200
3355
|
? { kind: "execution", reason: "explicit /run" }
|
|
@@ -3244,6 +3399,9 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
|
|
|
3244
3399
|
try {
|
|
3245
3400
|
const result = await this.handleConversationalAgentIntent(message, sessionId, agentIntent);
|
|
3246
3401
|
await this.sendMarkup(message.chat.id, result.markup, message.message_id);
|
|
3402
|
+
if (result.continuation) {
|
|
3403
|
+
await this.rememberContinuationWithOptions(message, result.continuation.text, { ...result.continuation, force: true });
|
|
3404
|
+
}
|
|
3247
3405
|
} catch (error) {
|
|
3248
3406
|
await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
|
|
3249
3407
|
}
|
|
@@ -3381,6 +3539,32 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
|
|
|
3381
3539
|
await this.sendMessage(message.chat.id, escapeTelegramHtml(gateReason), message.message_id, { parseMode: "HTML" });
|
|
3382
3540
|
return;
|
|
3383
3541
|
}
|
|
3542
|
+
const blockingReview = getLatestBlockingReviewPolicy(operatorGate.project);
|
|
3543
|
+
if (blockingReview) {
|
|
3544
|
+
const meta = blockingReview.meta && typeof blockingReview.meta === "object" ? blockingReview.meta : {};
|
|
3545
|
+
const reviewerName = String(meta.ownerName || meta.agentId || "assigned reviewer").trim();
|
|
3546
|
+
const followUp = `Blocking review is assigned to ${reviewerName}. Reply with override to proceed anyway, or reviewed when the review is done.`;
|
|
3547
|
+
await this.sendMarkup(
|
|
3548
|
+
message.chat.id,
|
|
3549
|
+
[
|
|
3550
|
+
"<b>Blocking review in effect</b>",
|
|
3551
|
+
`reviewer: <code>${escapeTelegramHtml(reviewerName)}</code>`,
|
|
3552
|
+
"Execution is paused until the blocking review is cleared or overridden.",
|
|
3553
|
+
followUp
|
|
3554
|
+
].join("\n"),
|
|
3555
|
+
message.message_id
|
|
3556
|
+
);
|
|
3557
|
+
await this.rememberContinuationWithOptions(message, followUp, {
|
|
3558
|
+
kind: "blocking-review-override",
|
|
3559
|
+
source: "review-policy-gate",
|
|
3560
|
+
context: {
|
|
3561
|
+
agentId: String(meta.agentId || "").trim(),
|
|
3562
|
+
ownerName: reviewerName
|
|
3563
|
+
},
|
|
3564
|
+
force: true
|
|
3565
|
+
});
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3384
3568
|
previewMessage = await this.sendProgressMessage(message.chat.id, message.message_id);
|
|
3385
3569
|
const content = (await this.runPromptViaBridge(message, sessionId, promptText, { explicitExecution: shouldExecutePrompt }))
|
|
3386
3570
|
?? (await this.runPromptFallback(sessionId, promptText));
|
package/src/shared-project.js
CHANGED
|
@@ -663,6 +663,22 @@ export async function upsertSharedAgent(cwd, agent = {}, options = {}) {
|
|
|
663
663
|
})).project;
|
|
664
664
|
}
|
|
665
665
|
|
|
666
|
+
export async function addSharedRoomNote(cwd, text = "", options = {}) {
|
|
667
|
+
const existing = await loadSharedProject(cwd);
|
|
668
|
+
requireSharedProject(existing);
|
|
669
|
+
const normalizedText = String(text || "").trim();
|
|
670
|
+
if (!normalizedText) throw new Error("room note text is required");
|
|
671
|
+
if (options.actorId) {
|
|
672
|
+
requireMember(existing, options.actorId);
|
|
673
|
+
}
|
|
674
|
+
return (await recordSharedProjectEvent(cwd, existing, normalizedText, {
|
|
675
|
+
type: String(options.type || "room-note").trim() || "room-note",
|
|
676
|
+
actorId: String(options.actorId || "").trim(),
|
|
677
|
+
actorName: String(options.actorName || "").trim(),
|
|
678
|
+
meta: options.meta && typeof options.meta === "object" ? options.meta : {}
|
|
679
|
+
})).project;
|
|
680
|
+
}
|
|
681
|
+
|
|
666
682
|
export async function createSharedInvite(cwd, member = {}, options = {}) {
|
|
667
683
|
const existing = await loadSharedProject(cwd);
|
|
668
684
|
requireOwner(existing, options.actorId);
|