@tritard/waterbrother 0.16.73 → 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.73",
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);
@@ -1293,9 +1293,15 @@ class TelegramGateway {
1293
1293
  }
1294
1294
 
1295
1295
  async rememberContinuation(message, text = "") {
1296
+ return this.rememberContinuationWithOptions(message, text);
1297
+ }
1298
+
1299
+ async rememberContinuationWithOptions(message, text = "", extra = {}) {
1296
1300
  const body = String(text || "").trim();
1297
1301
  if (!body) return;
1298
- const asksFollowUp =
1302
+ const asksFollowUp = extra.force === true
1303
+ ? true
1304
+ :
1299
1305
  /\?\s*$/.test(body)
1300
1306
  || /\b(would you like|do you want|which\b|what\b.*\?|how\b.*\?|who\b.*\?|where\b.*\?)\b/i.test(body);
1301
1307
  if (!asksFollowUp) return;
@@ -1304,8 +1310,9 @@ class TelegramGateway {
1304
1310
  chatId: String(message?.chat?.id || "").trim(),
1305
1311
  userId: String(message?.from?.id || "").trim(),
1306
1312
  lastPrompt: body.slice(0, 400),
1307
- kind: "follow-up",
1308
- 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 } : {},
1309
1316
  expiresAt: new Date(Date.now() + TELEGRAM_CONTINUATION_TTL_MS).toISOString()
1310
1317
  };
1311
1318
  await this.persistState();
@@ -1698,6 +1705,9 @@ class TelegramGateway {
1698
1705
  : intent.action === "agent-execute"
1699
1706
  ? "This sets the room executor role. Claim/mode rules still control actual execution."
1700
1707
  : "";
1708
+ const followUp = intent.action === "agent-review"
1709
+ ? `Should ${targetAgent.ownerName || targetAgent.label || "that terminal"} review be advisory or blocking?`
1710
+ : "";
1701
1711
  return {
1702
1712
  kind: "agent",
1703
1713
  project: nextProject,
@@ -1708,11 +1718,69 @@ class TelegramGateway {
1708
1718
  `role: <code>${escapeTelegramHtml(intent.role)}</code>`,
1709
1719
  `runtime: <code>${escapeTelegramHtml(runtimeLabel)}</code>`,
1710
1720
  `project: <code>${escapeTelegramHtml(nextProject.projectName || path.basename(session.cwd || this.cwd))}</code>`,
1711
- note
1712
- ].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
1713
1736
  };
1714
1737
  }
1715
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
+
1716
1784
  async handleStateIntent(message, sessionId, intent) {
1717
1785
  const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
1718
1786
  const cwd = session.cwd || this.cwd;
@@ -3195,6 +3263,21 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
3195
3263
  const isExplicitRun = text.startsWith("/run ");
3196
3264
  const rawPromptText = this.stripBotMention(isExplicitRun ? text.replace("/run", "").trim() : text);
3197
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
+ }
3198
3281
  const groupIntent = this.isGroupChat(message)
3199
3282
  ? (isExplicitRun
3200
3283
  ? { kind: "execution", reason: "explicit /run" }
@@ -3244,6 +3327,9 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
3244
3327
  try {
3245
3328
  const result = await this.handleConversationalAgentIntent(message, sessionId, agentIntent);
3246
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
+ }
3247
3333
  } catch (error) {
3248
3334
  await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
3249
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);