@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.73",
3
+ "version": "0.16.75",
4
4
  "description": "Waterbrother: bring-your-own-model coding CLI with local tools, sessions, operator modes, and approval controls",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- ].join("\n")
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));
@@ -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);