@tritard/waterbrother 0.16.22 → 0.16.23

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/README.md CHANGED
@@ -264,6 +264,7 @@ Rollout order:
264
264
  Shared project foundation is now live:
265
265
  - enable it with `waterbrother project share`
266
266
  - inspect it with `waterbrother room status`
267
+ - control conversation vs execution with `waterbrother room mode chat|plan|execute`
267
268
  - claim or release the shared operator lock with `waterbrother room claim` and `waterbrother room release`
268
269
  - shared project metadata lives in `.waterbrother/shared.json`
269
270
  - human collaboration notes live in `ROUNDTABLE.md`
@@ -276,7 +277,8 @@ Current Telegram behavior:
276
277
  - pending pairings are explicit and expire automatically after 12 hours unless approved
277
278
  - paired Telegram users drive the same live session and permissions as the terminal operator when the TUI bridge is attached
278
279
  - Telegram now supports remote workspace control with `/cwd`, `/use <path>`, `/desktop`, and `/new-project <name>`
279
- - shared projects now support `/room`, `/claim`, and `/release` from Telegram with a single active-operator lock
280
+ - shared projects now support `/room`, `/mode`, `/claim`, and `/release` from Telegram with a single active-operator lock
281
+ - shared Telegram execution only runs when the shared room is in `execute` mode
280
282
  - pairing is now explicit: first DM creates a pending request, then approve locally with `waterbrother gateway pair telegram <user-id>`
281
283
 
282
284
  ## Release flow
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.22",
3
+ "version": "0.16.23",
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": {
package/src/cli.js CHANGED
@@ -64,7 +64,8 @@ import {
64
64
  formatSharedProjectStatus,
65
65
  getSharedProjectPaths,
66
66
  loadSharedProject,
67
- releaseSharedOperator
67
+ releaseSharedOperator,
68
+ setSharedRoomMode
68
69
  } from "./shared-project.js";
69
70
 
70
71
  const execFileAsync = promisify(execFile);
@@ -147,6 +148,7 @@ const INTERACTIVE_COMMANDS = [
147
148
  { name: "/share-project", description: "Enable shared-project mode in the current cwd" },
148
149
  { name: "/unshare-project", description: "Disable shared-project mode in the current cwd" },
149
150
  { name: "/room", description: "Show shared room status for the current project" },
151
+ { name: "/room mode <chat|plan|execute>", description: "Set collaboration mode for the shared room" },
150
152
  { name: "/room claim", description: "Claim operator control for the shared room" },
151
153
  { name: "/room release", description: "Release operator control for the shared room" },
152
154
  { name: "/cwd", description: "Show current working directory" },
@@ -270,6 +272,7 @@ Usage:
270
272
  waterbrother project share
271
273
  waterbrother project unshare
272
274
  waterbrother room status
275
+ waterbrother room mode <chat|plan|execute>
273
276
  waterbrother room claim
274
277
  waterbrother room release
275
278
  waterbrother mcp list
@@ -3787,7 +3790,27 @@ async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false
3787
3790
  return;
3788
3791
  }
3789
3792
 
3790
- throw new Error("Usage: waterbrother room status|claim|release");
3793
+ if (sub === "mode") {
3794
+ const nextMode = String(positional[2] || "").trim().toLowerCase();
3795
+ if (!nextMode) {
3796
+ const project = await loadSharedProject(cwd);
3797
+ if (asJson) {
3798
+ printData({ ok: true, roomMode: project?.roomMode || null, project }, true);
3799
+ return;
3800
+ }
3801
+ console.log(project?.roomMode || "shared project: off");
3802
+ return;
3803
+ }
3804
+ const project = await setSharedRoomMode(cwd, nextMode);
3805
+ if (asJson) {
3806
+ printData({ ok: true, action: "mode", roomMode: project.roomMode, project }, true);
3807
+ return;
3808
+ }
3809
+ console.log(`Room mode set to ${project.roomMode}`);
3810
+ return;
3811
+ }
3812
+
3813
+ throw new Error("Usage: waterbrother room status|mode <chat|plan|execute>|claim|release");
3791
3814
  }
3792
3815
 
3793
3816
  async function runMcpCommand(positional, runtime, { cwd, asJson = false } = {}) {
@@ -7615,6 +7638,20 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
7615
7638
  continue;
7616
7639
  }
7617
7640
 
7641
+ if (line.startsWith("/room mode ")) {
7642
+ const nextMode = line.replace("/room mode", "").trim().toLowerCase();
7643
+ if (!nextMode) {
7644
+ console.log("Usage: /room mode <chat|plan|execute>");
7645
+ continue;
7646
+ }
7647
+ try {
7648
+ await runRoomCommand(["room", "mode", nextMode], { cwd: context.cwd, asJson: false });
7649
+ } catch (error) {
7650
+ console.log(`room mode failed: ${error instanceof Error ? error.message : String(error)}`);
7651
+ }
7652
+ continue;
7653
+ }
7654
+
7618
7655
  if (line === "/room claim") {
7619
7656
  try {
7620
7657
  await runRoomCommand(["room", "claim"], { cwd: context.cwd, asJson: false });
package/src/gateway.js CHANGED
@@ -8,7 +8,7 @@ import { createSession, listSessions, loadSession, saveSession } from "./session
8
8
  import { DEFAULT_PENDING_PAIRING_TTL_MINUTES, loadGatewayBridge, loadGatewayState, prunePendingPairings, saveGatewayBridge, saveGatewayState } from "./gateway-state.js";
9
9
  import { getGatewayStatus, getChannelSpec } from "./channels.js";
10
10
  import { canonicalizeLoosePath } from "./path-utils.js";
11
- import { claimSharedOperator, loadSharedProject, releaseSharedOperator, setSharedRoom } from "./shared-project.js";
11
+ import { claimSharedOperator, loadSharedProject, releaseSharedOperator, setSharedRoom, setSharedRoomMode } from "./shared-project.js";
12
12
 
13
13
  const execFileAsync = promisify(execFile);
14
14
  const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -26,6 +26,7 @@ const TELEGRAM_COMMANDS = [
26
26
  { command: "cwd", description: "Show the current remote working directory" },
27
27
  { command: "runtime", description: "Show active runtime status" },
28
28
  { command: "room", description: "Show shared room status" },
29
+ { command: "mode", description: "Show or set shared room mode" },
29
30
  { command: "claim", description: "Claim operator control for a shared project" },
30
31
  { command: "release", description: "Release operator control for a shared project" },
31
32
  { command: "sessions", description: "List recent remote sessions" },
@@ -198,6 +199,7 @@ function buildRemoteHelp() {
198
199
  "<code>/new-project &lt;name&gt;</code> create a folder on Desktop and switch into it",
199
200
  "<code>/runtime</code> show active provider/model/runtime state",
200
201
  "<code>/room</code> show shared project room status",
202
+ "<code>/mode</code> or <code>/mode &lt;chat|plan|execute&gt;</code> inspect or change shared room mode",
201
203
  "<code>/claim</code> claim operator control for a shared project",
202
204
  "<code>/release</code> release operator control for a shared project",
203
205
  "<code>/sessions</code> list recent linked remote sessions",
@@ -282,6 +284,7 @@ function formatTelegramRoomMarkup(project) {
282
284
  "<b>Shared room</b>",
283
285
  `project: <code>${escapeTelegramHtml(project.projectName || "")}</code>`,
284
286
  `mode: <code>${escapeTelegramHtml(project.mode || "single-operator")}</code>`,
287
+ `room mode: <code>${escapeTelegramHtml(project.roomMode || "chat")}</code>`,
285
288
  `room: <code>${escapeTelegramHtml(roomLabel)}</code>`,
286
289
  `active operator: <code>${escapeTelegramHtml(active)}</code>`,
287
290
  "<b>Members</b>",
@@ -620,6 +623,14 @@ class TelegramGateway {
620
623
  const { session, project } = await this.loadSharedProjectForSession(sessionId);
621
624
  if (!project?.enabled) return { ok: true, project: null, session };
622
625
  const bound = await this.bindSharedRoomForMessage(message, sessionId);
626
+ if (bound.project?.roomMode !== "execute") {
627
+ return {
628
+ ok: false,
629
+ project: bound.project,
630
+ session: bound.session,
631
+ reason: `Shared room is in ${bound.project?.roomMode || "chat"} mode. Switch to /mode execute before running code.`
632
+ };
633
+ }
623
634
  const userId = String(message?.from?.id || "").trim();
624
635
  if (!bound.project?.activeOperator?.id) {
625
636
  return { ok: false, project: bound.project, session: bound.session, reason: "No active operator. Use /claim first." };
@@ -869,6 +880,34 @@ class TelegramGateway {
869
880
  return;
870
881
  }
871
882
 
883
+ if (text === "/mode") {
884
+ const { project } = await this.bindSharedRoomForMessage(message, sessionId);
885
+ await this.sendMessage(
886
+ message.chat.id,
887
+ project?.enabled
888
+ ? `<b>Shared room mode</b>\n<code>${escapeTelegramHtml(project.roomMode || "chat")}</code>`
889
+ : "This project is not shared.",
890
+ message.message_id
891
+ );
892
+ return;
893
+ }
894
+
895
+ if (text.startsWith("/mode ")) {
896
+ const requestedMode = text.replace("/mode", "").trim().toLowerCase();
897
+ const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
898
+ if (!project?.enabled) {
899
+ await this.sendMessage(message.chat.id, "This project is not shared.", message.message_id);
900
+ return;
901
+ }
902
+ try {
903
+ const next = await setSharedRoomMode(session.cwd || this.cwd, requestedMode);
904
+ await this.sendMessage(message.chat.id, `Shared room mode set to <code>${escapeTelegramHtml(next.roomMode)}</code>`, message.message_id);
905
+ } catch (error) {
906
+ await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
907
+ }
908
+ return;
909
+ }
910
+
872
911
  if (text === "/claim") {
873
912
  const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
874
913
  if (!project?.enabled) {
@@ -37,6 +37,9 @@ function normalizeSharedProject(project = {}, cwd = process.cwd()) {
37
37
  title: String(project.room?.title || "").trim()
38
38
  },
39
39
  mode: String(project.mode || "single-operator").trim() || "single-operator",
40
+ roomMode: ["chat", "plan", "execute"].includes(String(project.roomMode || "").trim())
41
+ ? String(project.roomMode).trim()
42
+ : "chat",
40
43
  members,
41
44
  activeOperator: activeOperator?.id ? activeOperator : null,
42
45
  approvalPolicy: String(project.approvalPolicy || "owner").trim() || "owner",
@@ -85,6 +88,7 @@ function defaultRoundtableContent(project) {
85
88
  "## Project",
86
89
  `- Name: ${project.projectName}`,
87
90
  `- Mode: ${project.mode}`,
91
+ `- Room mode: ${project.roomMode}`,
88
92
  `- Approval policy: ${project.approvalPolicy}`,
89
93
  project.room?.provider ? `- Room: ${project.room.provider} ${project.room.chatId || ""}`.trim() : "- Room: not linked",
90
94
  "",
@@ -196,6 +200,21 @@ export async function setSharedRoom(cwd, room = {}) {
196
200
  return next;
197
201
  }
198
202
 
203
+ export async function setSharedRoomMode(cwd, roomMode = "chat") {
204
+ const existing = await loadSharedProject(cwd);
205
+ if (!existing?.enabled) throw new Error("Project is not shared.");
206
+ const normalized = String(roomMode || "").trim().toLowerCase();
207
+ if (!["chat", "plan", "execute"].includes(normalized)) {
208
+ throw new Error("Invalid room mode. Expected one of chat, plan, execute.");
209
+ }
210
+ const next = await saveSharedProject(cwd, {
211
+ ...existing,
212
+ roomMode: normalized
213
+ });
214
+ await appendRoundtableEvent(cwd, `- ${new Date().toISOString()}: room mode set to ${normalized}`);
215
+ return next;
216
+ }
217
+
199
218
  export async function claimSharedOperator(cwd, operator = {}) {
200
219
  const existing = await loadSharedProject(cwd);
201
220
  if (!existing?.enabled) throw new Error("Project is not shared.");
@@ -245,6 +264,7 @@ export function formatSharedProjectStatus(project) {
245
264
  cwd: project.cwd,
246
265
  room: project.room,
247
266
  mode: project.mode,
267
+ roomMode: project.roomMode,
248
268
  approvalPolicy: project.approvalPolicy,
249
269
  activeOperator: project.activeOperator,
250
270
  members: project.members