@tritard/waterbrother 0.16.72 → 0.16.74

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.72",
3
+ "version": "0.16.74",
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);
@@ -582,7 +582,9 @@ function parseTelegramAgentIntent(text = "") {
582
582
  if (/^(how|what|why|when|where|who)\b/.test(lowered)) return null;
583
583
  const reviewPatterns = [
584
584
  /^(?:have|ask|use)\s+(.+?)['’]s\s+(?:bot|terminal)\s+(?:to\s+)?review(?:\s+this)?\s*$/i,
585
- /^(?:have|ask|use)\s+(.+?)\s+(?:bot|terminal)\s+(?:to\s+)?review(?:\s+this)?\s*$/i
585
+ /^(?:have|ask|use)\s+(.+?)\s+(?:bot|terminal)\s+(?:to\s+)?review(?:\s+this)?\s*$/i,
586
+ /^(.+?),\s*review(?:\s+this)?\s+with\s+(?:your|ur)\s+(?:bot|terminal)\s*$/i,
587
+ /^(.+?)\s+should\s+review(?:\s+this)?\s+with\s+(?:their|his|her)\s+(?:bot|terminal)\s*$/i
586
588
  ];
587
589
  for (const pattern of reviewPatterns) {
588
590
  const match = value.match(pattern);
@@ -597,7 +599,9 @@ function parseTelegramAgentIntent(text = "") {
597
599
  /^(?:have|ask|use)\s+(.+?)['’]s\s+(?:bot|terminal)\s+(?:to\s+)?(?:take execution|execute|handle execution|take this)\s*$/i,
598
600
  /^(?:have|ask|use)\s+(.+?)\s+(?:bot|terminal)\s+(?:to\s+)?(?:take execution|execute|handle execution|take this)\s*$/i,
599
601
  /^(.+?)['’]s\s+(?:bot|terminal)\s+should\s+(?:take execution|execute|handle execution)\s*$/i,
600
- /^(.+?)\s+(?:bot|terminal)\s+should\s+(?:take execution|execute|handle execution)\s*$/i
602
+ /^(.+?)\s+(?:bot|terminal)\s+should\s+(?:take execution|execute|handle execution)\s*$/i,
603
+ /^(.+?),\s*(?:take execution|execute|handle execution|take this)\s+with\s+(?:your|ur)\s+(?:bot|terminal)\s*$/i,
604
+ /^(.+?)\s+should\s+(?:take execution|execute|handle execution)\s+with\s+(?:their|his|her)\s+(?:bot|terminal)\s*$/i
601
605
  ];
602
606
  for (const pattern of executePatterns) {
603
607
  const match = value.match(pattern);
@@ -1289,9 +1293,15 @@ class TelegramGateway {
1289
1293
  }
1290
1294
 
1291
1295
  async rememberContinuation(message, text = "") {
1296
+ return this.rememberContinuationWithOptions(message, text);
1297
+ }
1298
+
1299
+ async rememberContinuationWithOptions(message, text = "", extra = {}) {
1292
1300
  const body = String(text || "").trim();
1293
1301
  if (!body) return;
1294
- const asksFollowUp =
1302
+ const asksFollowUp = extra.force === true
1303
+ ? true
1304
+ :
1295
1305
  /\?\s*$/.test(body)
1296
1306
  || /\b(would you like|do you want|which\b|what\b.*\?|how\b.*\?|who\b.*\?|where\b.*\?)\b/i.test(body);
1297
1307
  if (!asksFollowUp) return;
@@ -1300,8 +1310,9 @@ class TelegramGateway {
1300
1310
  chatId: String(message?.chat?.id || "").trim(),
1301
1311
  userId: String(message?.from?.id || "").trim(),
1302
1312
  lastPrompt: body.slice(0, 400),
1303
- kind: "follow-up",
1304
- source: "assistant-question",
1313
+ kind: String(extra.kind || "follow-up").trim() || "follow-up",
1314
+ source: String(extra.source || "assistant-question").trim() || "assistant-question",
1315
+ context: extra.context && typeof extra.context === "object" ? { ...extra.context } : {},
1305
1316
  expiresAt: new Date(Date.now() + TELEGRAM_CONTINUATION_TTL_MS).toISOString()
1306
1317
  };
1307
1318
  await this.persistState();
@@ -1694,6 +1705,9 @@ class TelegramGateway {
1694
1705
  : intent.action === "agent-execute"
1695
1706
  ? "This sets the room executor role. Claim/mode rules still control actual execution."
1696
1707
  : "";
1708
+ const followUp = intent.action === "agent-review"
1709
+ ? `Should ${targetAgent.ownerName || targetAgent.label || "that terminal"} review be advisory or blocking?`
1710
+ : "";
1697
1711
  return {
1698
1712
  kind: "agent",
1699
1713
  project: nextProject,
@@ -1704,11 +1718,69 @@ class TelegramGateway {
1704
1718
  `role: <code>${escapeTelegramHtml(intent.role)}</code>`,
1705
1719
  `runtime: <code>${escapeTelegramHtml(runtimeLabel)}</code>`,
1706
1720
  `project: <code>${escapeTelegramHtml(nextProject.projectName || path.basename(session.cwd || this.cwd))}</code>`,
1707
- note
1708
- ].join("\n")
1721
+ note,
1722
+ followUp
1723
+ ].filter(Boolean).join("\n"),
1724
+ continuation: followUp
1725
+ ? {
1726
+ text: followUp,
1727
+ kind: "agent-review-policy",
1728
+ source: "agent-delegation",
1729
+ context: {
1730
+ agentId: String(targetAgent.id || "").trim(),
1731
+ ownerName: String(targetAgent.ownerName || targetAgent.label || "").trim(),
1732
+ projectName: String(nextProject.projectName || "").trim()
1733
+ }
1734
+ }
1735
+ : null
1709
1736
  };
1710
1737
  }
1711
1738
 
1739
+ async handleStructuredContinuation(message, sessionId, continuation, peer) {
1740
+ if (!continuation?.kind) return null;
1741
+ const reply = this.stripBotMention(String(message?.text || "").trim()).toLowerCase();
1742
+ if (continuation.kind === "agent-review-policy") {
1743
+ const policy = /\b(blocking|block|required|gate)\b/.test(reply)
1744
+ ? "blocking"
1745
+ : /\b(advisory|advice|optional|non-blocking|nonblocking)\b/.test(reply)
1746
+ ? "advisory"
1747
+ : "";
1748
+ if (!policy) {
1749
+ return {
1750
+ markup: `Reply with <code>advisory</code> or <code>blocking</code> for ${escapeTelegramHtml(continuation.context?.ownerName || "that review")}.`,
1751
+ remember: {
1752
+ text: String(continuation.lastPrompt || "").trim() || "Should this review be advisory or blocking?",
1753
+ kind: continuation.kind,
1754
+ source: continuation.source,
1755
+ context: continuation.context || {},
1756
+ force: true
1757
+ }
1758
+ };
1759
+ }
1760
+ const session = await loadSession(sessionId);
1761
+ const actorId = String(message?.from?.id || "").trim();
1762
+ const actorName = peer?.username || [message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim() || actorId;
1763
+ await addSharedRoomNote(session.cwd || this.cwd, `${continuation.context?.ownerName || "Assigned"} review should be ${policy}`, {
1764
+ actorId,
1765
+ actorName,
1766
+ type: "review-policy",
1767
+ meta: {
1768
+ agentId: String(continuation.context?.agentId || "").trim(),
1769
+ policy
1770
+ }
1771
+ });
1772
+ return {
1773
+ markup: [
1774
+ "<b>Review policy noted</b>",
1775
+ `reviewer: <code>${escapeTelegramHtml(continuation.context?.ownerName || "unknown")}</code>`,
1776
+ `policy: <code>${escapeTelegramHtml(policy)}</code>`,
1777
+ "This is recorded in the room as coordination guidance. Automatic enforcement is not wired yet."
1778
+ ].join("\n")
1779
+ };
1780
+ }
1781
+ return null;
1782
+ }
1783
+
1712
1784
  async handleStateIntent(message, sessionId, intent) {
1713
1785
  const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
1714
1786
  const cwd = session.cwd || this.cwd;
@@ -3191,6 +3263,21 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
3191
3263
  const isExplicitRun = text.startsWith("/run ");
3192
3264
  const rawPromptText = this.stripBotMention(isExplicitRun ? text.replace("/run", "").trim() : text);
3193
3265
  const promptText = continuation ? buildContinuationPrompt(rawPromptText, continuation) : rawPromptText;
3266
+ if (continuation?.kind && continuation.kind !== "follow-up") {
3267
+ try {
3268
+ const structured = await this.handleStructuredContinuation(message, sessionId, continuation, peer);
3269
+ if (structured?.markup) {
3270
+ await this.sendMarkup(message.chat.id, structured.markup, message.message_id);
3271
+ if (structured.remember) {
3272
+ await this.rememberContinuationWithOptions(message, structured.remember.text, structured.remember);
3273
+ }
3274
+ return;
3275
+ }
3276
+ } catch (error) {
3277
+ await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
3278
+ return;
3279
+ }
3280
+ }
3194
3281
  const groupIntent = this.isGroupChat(message)
3195
3282
  ? (isExplicitRun
3196
3283
  ? { kind: "execution", reason: "explicit /run" }
@@ -3240,6 +3327,9 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
3240
3327
  try {
3241
3328
  const result = await this.handleConversationalAgentIntent(message, sessionId, agentIntent);
3242
3329
  await this.sendMarkup(message.chat.id, result.markup, message.message_id);
3330
+ if (result.continuation) {
3331
+ await this.rememberContinuationWithOptions(message, result.continuation.text, { ...result.continuation, force: true });
3332
+ }
3243
3333
  } catch (error) {
3244
3334
  await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
3245
3335
  }
@@ -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);