@tritard/waterbrother 0.16.28 → 0.16.30

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
@@ -267,6 +267,8 @@ Shared project foundation is now live:
267
267
  - control conversation vs execution with `waterbrother room mode chat|plan|execute`
268
268
  - manage collaborators with `waterbrother room members`, `waterbrother room add`, and `waterbrother room remove`
269
269
  - manage the shared backlog with `waterbrother room tasks`, `waterbrother room task add`, and `waterbrother room task move`
270
+ - assign and claim shared work with `waterbrother room task assign` and `waterbrother room task claim`
271
+ - choose a shared execution preset with `waterbrother room runtime <profile>`
270
272
  - claim or release the shared operator lock with `waterbrother room claim` and `waterbrother room release`
271
273
  - shared project metadata lives in `.waterbrother/shared.json`
272
274
  - human collaboration notes live in `ROUNDTABLE.md`
@@ -279,10 +281,13 @@ Current Telegram behavior:
279
281
  - pending pairings are explicit and expire automatically after 12 hours unless approved
280
282
  - paired Telegram users drive the same live session and permissions as the terminal operator when the TUI bridge is attached
281
283
  - Telegram now supports remote workspace control with `/cwd`, `/use <path>`, `/desktop`, and `/new-project <name>`
282
- - shared projects now support `/room`, `/members`, `/tasks`, `/mode`, `/claim`, `/release`, `/invite`, `/remove-member`, and `/task ...` from Telegram
284
+ - shared projects now support `/room`, `/members`, `/tasks`, `/mode`, `/claim`, `/release`, `/invite`, `/remove-member`, `/room-runtime`, and `/task ...` from Telegram
283
285
  - shared Telegram execution only runs when the shared room is in `execute` mode
284
286
  - room administration is owner-only, and only owners/editors can hold the operator lock
287
+ - `/room` status now shows the active executor surface plus provider/model/runtime identity
285
288
  - in Telegram groups, Waterbrother only responds when directly targeted: slash commands, `@botname` mentions, or replies to a bot message
289
+ - in Telegram groups, directly targeted messages are now classified as chat, planning, or execution; only explicit execution requests run the live session
290
+ - in shared plan-mode groups, targeted `task:` and `todo:` messages are captured directly into the Roundtable task queue
286
291
  - pairing is now explicit: first DM creates a pending request, then approve locally with `waterbrother gateway pair telegram <user-id>`
287
292
 
288
293
  ## Release flow
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.28",
3
+ "version": "0.16.30",
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
@@ -59,7 +59,9 @@ import { formatPlanForDisplay } from "./planner.js";
59
59
  import { parseCharterFromGoal, runExperimentLoop, formatExperimentSummary, gitReturnToBranch } from "./experiment.js";
60
60
  import {
61
61
  addSharedTask,
62
+ assignSharedTask,
62
63
  claimSharedOperator,
64
+ claimSharedTask,
63
65
  disableSharedProject,
64
66
  enableSharedProject,
65
67
  formatSharedProjectStatus,
@@ -70,6 +72,7 @@ import {
70
72
  moveSharedTask,
71
73
  releaseSharedOperator,
72
74
  removeSharedMember,
75
+ setSharedRuntimeProfile,
73
76
  setSharedRoomMode,
74
77
  upsertSharedMember
75
78
  } from "./shared-project.js";
@@ -159,7 +162,11 @@ const INTERACTIVE_COMMANDS = [
159
162
  { name: "/room remove <id>", description: "Remove a shared-project member" },
160
163
  { name: "/room tasks", description: "List Roundtable tasks for the shared project" },
161
164
  { name: "/room task add <text>", description: "Add a Roundtable task" },
165
+ { name: "/room task assign <id> <member-id>", description: "Assign a Roundtable task to a shared member" },
166
+ { name: "/room task claim <id>", description: "Claim a Roundtable task for yourself" },
162
167
  { name: "/room task move <id> <open|active|blocked|done>", description: "Move a Roundtable task between states" },
168
+ { name: "/room runtime", description: "Show the shared room runtime profile" },
169
+ { name: "/room runtime <name|clear>", description: "Set or clear the shared room runtime profile" },
163
170
  { name: "/room mode <chat|plan|execute>", description: "Set collaboration mode for the shared room" },
164
171
  { name: "/room claim", description: "Claim operator control for the shared room" },
165
172
  { name: "/room release", description: "Release operator control for the shared room" },
@@ -289,7 +296,10 @@ Usage:
289
296
  waterbrother room remove <member-id>
290
297
  waterbrother room tasks
291
298
  waterbrother room task add <text>
299
+ waterbrother room task assign <id> <member-id>
300
+ waterbrother room task claim <id>
292
301
  waterbrother room task move <id> <open|active|blocked|done>
302
+ waterbrother room runtime [<name>|clear]
293
303
  waterbrother room mode <chat|plan|execute>
294
304
  waterbrother room claim
295
305
  waterbrother room release
@@ -2612,6 +2622,24 @@ function buildRuntimeStatusPayload(runtime, agent, currentSession = null) {
2612
2622
  };
2613
2623
  }
2614
2624
 
2625
+ function buildRoomStatusPayload(project, runtime = null, currentSession = null) {
2626
+ if (!project) {
2627
+ return { enabled: false };
2628
+ }
2629
+ return {
2630
+ ...project,
2631
+ executor: {
2632
+ surface: "local-tui",
2633
+ roomRuntimeProfile: project.runtimeProfile || "",
2634
+ runtimeProfile: currentSession?.runtimeProfile || runtime?.gateway?.defaultRuntimeProfile || "",
2635
+ provider: runtime?.provider || "unknown",
2636
+ model: runtime?.model || "unknown",
2637
+ designModel: runtime?.designModel || "unknown",
2638
+ agentProfile: runtime?.agentProfile || "coder"
2639
+ }
2640
+ };
2641
+ }
2642
+
2615
2643
  async function applyRuntimeSelection({
2616
2644
  config,
2617
2645
  context,
@@ -3774,16 +3802,17 @@ async function runProjectCommand(positional, { cwd = process.cwd(), asJson = fal
3774
3802
  throw new Error("Usage: waterbrother project share|unshare");
3775
3803
  }
3776
3804
 
3777
- async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false } = {}) {
3805
+ async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false, runtime = null, currentSession = null, agent = null, context = null } = {}) {
3778
3806
  const sub = String(positional[1] || "status").trim().toLowerCase();
3779
3807
  const operator = getLocalOperatorIdentity();
3780
3808
  if (sub === "status") {
3781
3809
  const project = await loadSharedProject(cwd);
3810
+ const payload = buildRoomStatusPayload(project, runtime, currentSession);
3782
3811
  if (asJson) {
3783
- printData(project || { enabled: false, cwd }, true);
3812
+ printData(payload, true);
3784
3813
  return;
3785
3814
  }
3786
- console.log(formatSharedProjectStatus(project));
3815
+ console.log(JSON.stringify(payload, null, 2));
3787
3816
  return;
3788
3817
  }
3789
3818
 
@@ -3814,7 +3843,7 @@ async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false
3814
3843
  return;
3815
3844
  }
3816
3845
  for (const task of tasks) {
3817
- console.log(`${task.id}\t${task.state}\t${task.text}`);
3846
+ console.log(`${task.id}\t${task.state}\t${task.assignedTo || "-"}\t${task.text}`);
3818
3847
  }
3819
3848
  return;
3820
3849
  }
@@ -3848,7 +3877,34 @@ async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false
3848
3877
  console.log(`Moved shared task [${result.task.id}] to ${result.task.state}`);
3849
3878
  return;
3850
3879
  }
3851
- throw new Error("Usage: waterbrother room task add <text>|move <id> <open|active|blocked|done>");
3880
+ if (action === "assign") {
3881
+ const taskId = String(positional[3] || "").trim();
3882
+ const memberId = String(positional[4] || "").trim();
3883
+ if (!taskId || !memberId) {
3884
+ throw new Error("Usage: waterbrother room task assign <id> <member-id>");
3885
+ }
3886
+ const result = await assignSharedTask(cwd, taskId, memberId, { actorId: operator.id });
3887
+ if (asJson) {
3888
+ printData({ ok: true, action: "task-assign", task: result.task, project: result.project }, true);
3889
+ return;
3890
+ }
3891
+ console.log(`Assigned shared task [${result.task.id}] to ${memberId}`);
3892
+ return;
3893
+ }
3894
+ if (action === "claim") {
3895
+ const taskId = String(positional[3] || "").trim();
3896
+ if (!taskId) {
3897
+ throw new Error("Usage: waterbrother room task claim <id>");
3898
+ }
3899
+ const result = await claimSharedTask(cwd, taskId, { actorId: operator.id });
3900
+ if (asJson) {
3901
+ printData({ ok: true, action: "task-claim", task: result.task, project: result.project }, true);
3902
+ return;
3903
+ }
3904
+ console.log(`Claimed shared task [${result.task.id}]`);
3905
+ return;
3906
+ }
3907
+ throw new Error("Usage: waterbrother room task add <text>|assign <id> <member-id>|claim <id>|move <id> <open|active|blocked|done>");
3852
3908
  }
3853
3909
 
3854
3910
  if (sub === "add") {
@@ -3925,7 +3981,35 @@ async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false
3925
3981
  return;
3926
3982
  }
3927
3983
 
3928
- throw new Error("Usage: waterbrother room status|members|add <member-id> [owner|editor|observer] [display name]|remove <member-id>|tasks|task add <text>|task move <id> <open|active|blocked|done>|mode <chat|plan|execute>|claim|release");
3984
+ if (sub === "runtime") {
3985
+ const nextProfile = String(positional[2] || "").trim();
3986
+ if (!nextProfile) {
3987
+ const project = await loadSharedProject(cwd);
3988
+ if (asJson) {
3989
+ printData({ ok: true, runtimeProfile: project?.runtimeProfile || "", project }, true);
3990
+ return;
3991
+ }
3992
+ console.log(project?.runtimeProfile || "none");
3993
+ return;
3994
+ }
3995
+ const normalized = nextProfile.toLowerCase() === "clear" ? "" : nextProfile;
3996
+ const project = await setSharedRuntimeProfile(cwd, normalized, { actorId: operator.id });
3997
+ if (normalized && agent && context && currentSession) {
3998
+ const { config } = await loadConfigLayers(cwd);
3999
+ const loaded = await loadNamedRuntimeProfile(normalized, { cwd, config, runtime: context.runtime, context, agent, session: currentSession });
4000
+ if (!loaded) {
4001
+ throw new Error(`runtime profile "${normalized}" not found`);
4002
+ }
4003
+ }
4004
+ if (asJson) {
4005
+ printData({ ok: true, action: "runtime", runtimeProfile: project.runtimeProfile, project }, true);
4006
+ return;
4007
+ }
4008
+ console.log(project.runtimeProfile ? `Room runtime profile set to ${project.runtimeProfile}` : "Room runtime profile cleared");
4009
+ return;
4010
+ }
4011
+
4012
+ throw new Error("Usage: waterbrother room status|members|add <member-id> [owner|editor|observer] [display name]|remove <member-id>|tasks|task add <text>|task assign <id> <member-id>|task claim <id>|task move <id> <open|active|blocked|done>|runtime [<name>|clear]|mode <chat|plan|execute>|claim|release");
3929
4013
  }
3930
4014
 
3931
4015
  async function runMcpCommand(positional, runtime, { cwd, asJson = false } = {}) {
@@ -7114,6 +7198,20 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
7114
7198
 
7115
7199
  remoteSessionId = currentSession.id;
7116
7200
  await touchTelegramBridgeHost({ sessionId: currentSession.id, cwd: context.cwd });
7201
+ if (remoteRequest.runtimeProfile && remoteRequest.runtimeProfile !== currentSession.runtimeProfile) {
7202
+ const { config } = await loadConfigLayers(context.cwd);
7203
+ const loaded = await loadNamedRuntimeProfile(remoteRequest.runtimeProfile, {
7204
+ cwd: context.cwd,
7205
+ config,
7206
+ runtime: context.runtime,
7207
+ context,
7208
+ agent,
7209
+ session: currentSession
7210
+ });
7211
+ if (loaded) {
7212
+ console.log(cyan(`telegram runtime -> ${remoteRequest.runtimeProfile}`));
7213
+ }
7214
+ }
7117
7215
  console.log(cyan(`telegram request from ${remoteActor}`));
7118
7216
  console.log(`${cyan("telegram>")} ${sanitizeTerminalText(remoteRequest.text || "")}`);
7119
7217
  await runTextTurnInteractive({
@@ -7746,7 +7844,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
7746
7844
 
7747
7845
  if (line === "/room") {
7748
7846
  try {
7749
- await runRoomCommand(["room", "status"], { cwd: context.cwd, asJson: false });
7847
+ await runRoomCommand(["room", "status"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
7750
7848
  } catch (error) {
7751
7849
  console.log(`room status failed: ${error instanceof Error ? error.message : String(error)}`);
7752
7850
  }
@@ -7755,7 +7853,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
7755
7853
 
7756
7854
  if (line === "/room members") {
7757
7855
  try {
7758
- await runRoomCommand(["room", "members"], { cwd: context.cwd, asJson: false });
7856
+ await runRoomCommand(["room", "members"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
7759
7857
  } catch (error) {
7760
7858
  console.log(`room members failed: ${error instanceof Error ? error.message : String(error)}`);
7761
7859
  }
@@ -7764,7 +7862,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
7764
7862
 
7765
7863
  if (line === "/room tasks") {
7766
7864
  try {
7767
- await runRoomCommand(["room", "tasks"], { cwd: context.cwd, asJson: false });
7865
+ await runRoomCommand(["room", "tasks"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
7768
7866
  } catch (error) {
7769
7867
  console.log(`room tasks failed: ${error instanceof Error ? error.message : String(error)}`);
7770
7868
  }
@@ -7787,7 +7885,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
7787
7885
  try {
7788
7886
  const positional = ["room", "add", memberId, role];
7789
7887
  if (displayName) positional.push(...displayName.split(" "));
7790
- await runRoomCommand(positional, { cwd: context.cwd, asJson: false });
7888
+ await runRoomCommand(positional, { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
7791
7889
  } catch (error) {
7792
7890
  console.log(`room add failed: ${error instanceof Error ? error.message : String(error)}`);
7793
7891
  }
@@ -7801,7 +7899,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
7801
7899
  continue;
7802
7900
  }
7803
7901
  try {
7804
- await runRoomCommand(["room", "remove", memberId], { cwd: context.cwd, asJson: false });
7902
+ await runRoomCommand(["room", "remove", memberId], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
7805
7903
  } catch (error) {
7806
7904
  console.log(`room remove failed: ${error instanceof Error ? error.message : String(error)}`);
7807
7905
  }
@@ -7815,7 +7913,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
7815
7913
  continue;
7816
7914
  }
7817
7915
  try {
7818
- await runRoomCommand(["room", "task", "add", ...text.split(" ")], { cwd: context.cwd, asJson: false });
7916
+ await runRoomCommand(["room", "task", "add", ...text.split(" ")], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
7819
7917
  } catch (error) {
7820
7918
  console.log(`room task add failed: ${error instanceof Error ? error.message : String(error)}`);
7821
7919
  }
@@ -7830,13 +7928,42 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
7830
7928
  continue;
7831
7929
  }
7832
7930
  try {
7833
- await runRoomCommand(["room", "task", "move", taskId, state], { cwd: context.cwd, asJson: false });
7931
+ await runRoomCommand(["room", "task", "move", taskId, state], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
7834
7932
  } catch (error) {
7835
7933
  console.log(`room task move failed: ${error instanceof Error ? error.message : String(error)}`);
7836
7934
  }
7837
7935
  continue;
7838
7936
  }
7839
7937
 
7938
+ if (line.startsWith("/room task assign ")) {
7939
+ const raw = line.replace("/room task assign", "").trim();
7940
+ const [taskId, memberId] = raw.split(/\s+/, 2);
7941
+ if (!taskId || !memberId) {
7942
+ console.log("Usage: /room task assign <id> <member-id>");
7943
+ continue;
7944
+ }
7945
+ try {
7946
+ await runRoomCommand(["room", "task", "assign", taskId, memberId], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
7947
+ } catch (error) {
7948
+ console.log(`room task assign failed: ${error instanceof Error ? error.message : String(error)}`);
7949
+ }
7950
+ continue;
7951
+ }
7952
+
7953
+ if (line.startsWith("/room task claim ")) {
7954
+ const taskId = line.replace("/room task claim", "").trim();
7955
+ if (!taskId) {
7956
+ console.log("Usage: /room task claim <id>");
7957
+ continue;
7958
+ }
7959
+ try {
7960
+ await runRoomCommand(["room", "task", "claim", taskId], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
7961
+ } catch (error) {
7962
+ console.log(`room task claim failed: ${error instanceof Error ? error.message : String(error)}`);
7963
+ }
7964
+ continue;
7965
+ }
7966
+
7840
7967
  if (line.startsWith("/room mode ")) {
7841
7968
  const nextMode = line.replace("/room mode", "").trim().toLowerCase();
7842
7969
  if (!nextMode) {
@@ -7844,16 +7971,26 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
7844
7971
  continue;
7845
7972
  }
7846
7973
  try {
7847
- await runRoomCommand(["room", "mode", nextMode], { cwd: context.cwd, asJson: false });
7974
+ await runRoomCommand(["room", "mode", nextMode], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
7848
7975
  } catch (error) {
7849
7976
  console.log(`room mode failed: ${error instanceof Error ? error.message : String(error)}`);
7850
7977
  }
7851
7978
  continue;
7852
7979
  }
7853
7980
 
7981
+ if (line === "/room runtime" || line.startsWith("/room runtime ")) {
7982
+ const value = line === "/room runtime" ? "" : line.replace("/room runtime", "").trim();
7983
+ try {
7984
+ await runRoomCommand(value ? ["room", "runtime", value] : ["room", "runtime"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
7985
+ } catch (error) {
7986
+ console.log(`room runtime failed: ${error instanceof Error ? error.message : String(error)}`);
7987
+ }
7988
+ continue;
7989
+ }
7990
+
7854
7991
  if (line === "/room claim") {
7855
7992
  try {
7856
- await runRoomCommand(["room", "claim"], { cwd: context.cwd, asJson: false });
7993
+ await runRoomCommand(["room", "claim"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
7857
7994
  } catch (error) {
7858
7995
  console.log(`room claim failed: ${error instanceof Error ? error.message : String(error)}`);
7859
7996
  }
@@ -7862,7 +7999,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
7862
7999
 
7863
8000
  if (line === "/room release") {
7864
8001
  try {
7865
- await runRoomCommand(["room", "release"], { cwd: context.cwd, asJson: false });
8002
+ await runRoomCommand(["room", "release"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
7866
8003
  } catch (error) {
7867
8004
  console.log(`room release failed: ${error instanceof Error ? error.message : String(error)}`);
7868
8005
  }
@@ -9691,7 +9828,7 @@ export async function runCli(argv) {
9691
9828
  }
9692
9829
 
9693
9830
  if (command === "room") {
9694
- await runRoomCommand(positional, { cwd: startupCwd, asJson });
9831
+ await runRoomCommand(positional, { cwd: startupCwd, asJson, runtime });
9695
9832
  return;
9696
9833
  }
9697
9834
 
@@ -33,6 +33,7 @@ function normalizeBridgeRequest(parsed = {}) {
33
33
  username: String(parsed?.username || "").trim(),
34
34
  sessionId: String(parsed?.sessionId || "").trim(),
35
35
  text: String(parsed?.text || "").trim(),
36
+ runtimeProfile: String(parsed?.runtimeProfile || "").trim(),
36
37
  replyToMessageId: Number.isFinite(Number(parsed?.replyToMessageId)) ? Math.floor(Number(parsed.replyToMessageId)) : 0,
37
38
  requestedAt: String(parsed?.requestedAt || "").trim(),
38
39
  status: String(parsed?.status || "pending").trim() || "pending",
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 { addSharedTask, claimSharedOperator, getSharedMember, listSharedTasks, loadSharedProject, moveSharedTask, releaseSharedOperator, setSharedRoom, setSharedRoomMode, upsertSharedMember, removeSharedMember } from "./shared-project.js";
11
+ import { addSharedTask, assignSharedTask, claimSharedOperator, claimSharedTask, getSharedMember, listSharedTasks, loadSharedProject, moveSharedTask, releaseSharedOperator, setSharedRoom, setSharedRoomMode, setSharedRuntimeProfile, upsertSharedMember, removeSharedMember } 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)), "..");
@@ -28,6 +28,7 @@ const TELEGRAM_COMMANDS = [
28
28
  { command: "room", description: "Show shared room status" },
29
29
  { command: "members", description: "List shared room members" },
30
30
  { command: "tasks", description: "List shared Roundtable tasks" },
31
+ { command: "room_runtime", description: "Show or set shared room runtime profile" },
31
32
  { command: "mode", description: "Show or set shared room mode" },
32
33
  { command: "claim", description: "Claim operator control for a shared project" },
33
34
  { command: "release", description: "Release operator control for a shared project" },
@@ -204,7 +205,10 @@ function buildRemoteHelp() {
204
205
  "<code>/members</code> list shared project members",
205
206
  "<code>/tasks</code> list shared project tasks",
206
207
  "<code>/task add &lt;text&gt;</code> add a shared Roundtable task",
208
+ "<code>/task assign &lt;id&gt; &lt;member-id&gt;</code> assign a shared task",
209
+ "<code>/task claim &lt;id&gt;</code> claim a shared task for yourself",
207
210
  "<code>/task move &lt;id&gt; &lt;open|active|blocked|done&gt;</code> move a shared Roundtable task",
211
+ "<code>/room-runtime</code> or <code>/room-runtime &lt;name|clear&gt;</code> inspect or set the shared room runtime profile",
208
212
  "<code>/invite &lt;user-id&gt; [owner|editor|observer]</code> add or update a shared project member",
209
213
  "<code>/remove-member &lt;user-id&gt;</code> remove a shared project member",
210
214
  "<code>/mode</code> or <code>/mode &lt;chat|plan|execute&gt;</code> inspect or change shared room mode",
@@ -274,7 +278,7 @@ function formatSessionListMarkup(currentSessionId, sessions = []) {
274
278
  return lines.join("\n");
275
279
  }
276
280
 
277
- function formatTelegramRoomMarkup(project) {
281
+ function formatTelegramRoomMarkup(project, options = {}) {
278
282
  if (!project?.enabled) {
279
283
  return "<b>Shared room</b>\nThis project is not shared.";
280
284
  }
@@ -288,6 +292,21 @@ function formatTelegramRoomMarkup(project) {
288
292
  const memberLines = members.length
289
293
  ? members.map((member) => `• ${escapeTelegramHtml(member.name || member.id)} <i>(${escapeTelegramHtml(member.role || "editor")})</i>`).join("\n")
290
294
  : "• none";
295
+ const executor = options.executor || {};
296
+ const executorBits = [
297
+ `surface: <code>${escapeTelegramHtml(executor.surface || "telegram")}</code>`,
298
+ `provider: <code>${escapeTelegramHtml(executor.provider || "unknown")}</code>`,
299
+ `model: <code>${escapeTelegramHtml(executor.model || "unknown")}</code>`
300
+ ];
301
+ if (project.runtimeProfile) {
302
+ executorBits.unshift(`room runtime: <code>${escapeTelegramHtml(project.runtimeProfile)}</code>`);
303
+ }
304
+ if (executor.runtimeProfile) {
305
+ executorBits.push(`runtime profile: <code>${escapeTelegramHtml(executor.runtimeProfile)}</code>`);
306
+ }
307
+ if (executor.hostSessionId) {
308
+ executorBits.push(`host session: <code>${escapeTelegramHtml(executor.hostSessionId)}</code>`);
309
+ }
291
310
  return [
292
311
  "<b>Shared room</b>",
293
312
  `project: <code>${escapeTelegramHtml(project.projectName || "")}</code>`,
@@ -295,6 +314,8 @@ function formatTelegramRoomMarkup(project) {
295
314
  `room mode: <code>${escapeTelegramHtml(project.roomMode || "chat")}</code>`,
296
315
  `room: <code>${escapeTelegramHtml(roomLabel)}</code>`,
297
316
  `active operator: <code>${escapeTelegramHtml(active)}</code>`,
317
+ "<b>Executor</b>",
318
+ ...executorBits,
298
319
  "<b>Members</b>",
299
320
  memberLines
300
321
  ].join("\n");
@@ -320,10 +341,48 @@ function formatTelegramTasksMarkup(tasks = []) {
320
341
  }
321
342
  return [
322
343
  "<b>Shared tasks</b>",
323
- ...tasks.map((task) => `• <code>${escapeTelegramHtml(task.id)}</code> <i>(${escapeTelegramHtml(task.state)})</i> ${escapeTelegramHtml(task.text)}`)
344
+ ...tasks.map((task) => `• <code>${escapeTelegramHtml(task.id)}</code> <i>(${escapeTelegramHtml(task.state)})</i> ${escapeTelegramHtml(task.text)}${task.assignedTo ? ` <code>@${escapeTelegramHtml(task.assignedTo)}</code>` : ""}`)
324
345
  ].join("\n");
325
346
  }
326
347
 
348
+ function classifyTelegramGroupIntent(text = "") {
349
+ const normalized = String(text || "").trim();
350
+ const lower = normalized.toLowerCase();
351
+ if (!normalized) return { kind: "chat", reason: "empty" };
352
+ if (/^(hi|hello|hey|yo|thanks|thank you|cool|nice|ok|okay|k)\b/.test(lower)) {
353
+ return { kind: "chat", reason: "casual language" };
354
+ }
355
+ if (
356
+ lower.includes("brainstorm")
357
+ || lower.includes("what do you think")
358
+ || lower.includes("should we")
359
+ || lower.includes("could we")
360
+ || lower.includes("let's think")
361
+ || lower.includes("plan this")
362
+ || lower.includes("ideas for")
363
+ || normalized.endsWith("?")
364
+ ) {
365
+ return { kind: "plan", reason: "planning language" };
366
+ }
367
+ if (
368
+ /^(make|create|build|implement|fix|change|update|add|remove|delete|rename|refactor|write|edit|move|run|search|open|use|switch|generate|ship|deploy|debug|investigate|audit)\b/.test(lower)
369
+ || /[`/].+/.test(normalized)
370
+ || /\b(file|folder|directory|repo|project|code|command|function|component|page|route|api|schema|bug)\b/.test(lower)
371
+ ) {
372
+ return { kind: "execution", reason: "explicit work request" };
373
+ }
374
+ return { kind: "chat", reason: "default non-execution" };
375
+ }
376
+
377
+ function suggestTaskText(text = "") {
378
+ return String(text || "")
379
+ .replace(/^@[\w_]+\s*/g, "")
380
+ .replace(/^(should we|could we|what do you think about|brainstorm|plan)\s+/i, "")
381
+ .replace(/[?]+$/g, "")
382
+ .trim()
383
+ .slice(0, 180);
384
+ }
385
+
327
386
  function parseInviteCommand(text) {
328
387
  const parts = String(text || "").trim().split(/\s+/).filter(Boolean);
329
388
  const userId = String(parts[1] || "").trim();
@@ -696,6 +755,19 @@ class TelegramGateway {
696
755
  return { session, project: next };
697
756
  }
698
757
 
758
+ async buildRoomMarkup(message, sessionId) {
759
+ const { project } = await this.bindSharedRoomForMessage(message, sessionId);
760
+ const host = await this.getLiveBridgeHost();
761
+ const executor = {
762
+ surface: host ? "live-tui" : "telegram-fallback",
763
+ provider: this.runtime.provider,
764
+ model: this.runtime.model,
765
+ runtimeProfile: project?.runtimeProfile || this.channel.defaultRuntimeProfile || this.gateway.defaultRuntimeProfile || "",
766
+ hostSessionId: host?.sessionId || ""
767
+ };
768
+ return project?.enabled ? formatTelegramRoomMarkup(project, { executor }) : "<b>Shared room</b>\nThis project is not shared.";
769
+ }
770
+
699
771
  async ensureSharedOperator(message, sessionId) {
700
772
  const { session, project } = await this.loadSharedProjectForSession(sessionId);
701
773
  if (!project?.enabled) return { ok: true, project: null, session };
@@ -750,6 +822,7 @@ class TelegramGateway {
750
822
  }
751
823
 
752
824
  const requestId = createBridgeRequestId();
825
+ const { project } = await this.bindSharedRoomForMessage(message, sessionId);
753
826
  const bridge = await loadGatewayBridge("telegram");
754
827
  const requests = Array.isArray(bridge.pendingRequests) ? bridge.pendingRequests : [];
755
828
  requests.push({
@@ -759,6 +832,7 @@ class TelegramGateway {
759
832
  username: [message?.from?.username, message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim(),
760
833
  sessionId: String(sessionId || "").trim(),
761
834
  text: String(promptText || "").trim(),
835
+ runtimeProfile: String(project?.runtimeProfile || "").trim(),
762
836
  replyToMessageId: message.message_id,
763
837
  requestedAt: new Date().toISOString(),
764
838
  status: "pending",
@@ -819,10 +893,25 @@ class TelegramGateway {
819
893
  }
820
894
 
821
895
  async runPromptFallback(sessionId, promptText) {
896
+ let providerArgs = [];
897
+ try {
898
+ const session = await loadSession(sessionId);
899
+ const project = await loadSharedProject(session.cwd || this.cwd);
900
+ const profileName = String(project?.runtimeProfile || "").trim();
901
+ const profile = profileName ? this.runtime.runtimeProfiles?.[profileName] : null;
902
+ if (profile) {
903
+ if (profile.provider) providerArgs.push("--provider", String(profile.provider));
904
+ if (profile.model) providerArgs.push("--model", String(profile.model));
905
+ if (profile.designModel) providerArgs.push("--design-model", String(profile.designModel));
906
+ if (profile.baseUrl) providerArgs.push("--base-url", String(profile.baseUrl));
907
+ }
908
+ } catch {}
909
+
822
910
  const { stdout } = await this.runWaterbrother([
823
911
  "resume",
824
912
  sessionId,
825
913
  promptText,
914
+ ...providerArgs,
826
915
  "--skip-onboarding",
827
916
  "--approval",
828
917
  "never",
@@ -953,8 +1042,7 @@ class TelegramGateway {
953
1042
  }
954
1043
 
955
1044
  if (text === "/room") {
956
- const { project } = await this.bindSharedRoomForMessage(message, sessionId);
957
- await this.sendMessage(message.chat.id, project?.enabled ? formatTelegramRoomMarkup(project) : "<b>Shared room</b>\nThis project is not shared.", message.message_id);
1045
+ await this.sendMessage(message.chat.id, await this.buildRoomMarkup(message, sessionId), message.message_id);
958
1046
  return;
959
1047
  }
960
1048
 
@@ -975,6 +1063,26 @@ class TelegramGateway {
975
1063
  return;
976
1064
  }
977
1065
 
1066
+ if (text.startsWith("/task ")) {
1067
+ const taskShortcut = text.replace("/task", "").trim().toLowerCase();
1068
+ if (taskShortcut === "") {
1069
+ await this.sendMessage(message.chat.id, "Usage: /task add <text> | /task assign <id> <member-id> | /task claim <id> | /task move <id> <open|active|blocked|done>", message.message_id);
1070
+ return;
1071
+ }
1072
+ }
1073
+
1074
+ if (text === "/room-runtime") {
1075
+ const { project } = await this.bindSharedRoomForMessage(message, sessionId);
1076
+ await this.sendMessage(
1077
+ message.chat.id,
1078
+ project?.enabled
1079
+ ? `<b>Shared room runtime profile</b>\n<code>${escapeTelegramHtml(project.runtimeProfile || "none")}</code>`
1080
+ : "This project is not shared.",
1081
+ message.message_id
1082
+ );
1083
+ return;
1084
+ }
1085
+
978
1086
  if (text === "/mode") {
979
1087
  const { project } = await this.bindSharedRoomForMessage(message, sessionId);
980
1088
  await this.sendMessage(
@@ -1003,6 +1111,29 @@ class TelegramGateway {
1003
1111
  return;
1004
1112
  }
1005
1113
 
1114
+ if (text.startsWith("/room-runtime ")) {
1115
+ const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
1116
+ if (!project?.enabled) {
1117
+ await this.sendMessage(message.chat.id, "This project is not shared.", message.message_id);
1118
+ return;
1119
+ }
1120
+ const requestedProfile = text.replace("/room-runtime", "").trim();
1121
+ const normalized = requestedProfile.toLowerCase() === "clear" ? "" : requestedProfile;
1122
+ try {
1123
+ const next = await setSharedRuntimeProfile(session.cwd || this.cwd, normalized, { actorId: userId });
1124
+ await this.sendMessage(
1125
+ message.chat.id,
1126
+ next.runtimeProfile
1127
+ ? `Shared room runtime profile set to <code>${escapeTelegramHtml(next.runtimeProfile)}</code>`
1128
+ : "Shared room runtime profile cleared.",
1129
+ message.message_id
1130
+ );
1131
+ } catch (error) {
1132
+ await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
1133
+ }
1134
+ return;
1135
+ }
1136
+
1006
1137
  if (text === "/claim") {
1007
1138
  const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
1008
1139
  if (!project?.enabled) {
@@ -1029,7 +1160,7 @@ class TelegramGateway {
1029
1160
  }
1030
1161
  try {
1031
1162
  const released = await releaseSharedOperator(session.cwd || this.cwd, userId, { actorId: userId });
1032
- await this.sendMessage(message.chat.id, released.activeOperator?.id ? formatTelegramRoomMarkup(released) : "Shared room released.", message.message_id);
1163
+ await this.sendMessage(message.chat.id, released.activeOperator?.id ? await this.buildRoomMarkup(message, sessionId) : "Shared room released.", message.message_id);
1033
1164
  } catch (error) {
1034
1165
  await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
1035
1166
  }
@@ -1129,6 +1260,46 @@ class TelegramGateway {
1129
1260
  return;
1130
1261
  }
1131
1262
 
1263
+ if (text.startsWith("/task assign ")) {
1264
+ const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
1265
+ if (!project?.enabled) {
1266
+ await this.sendMessage(message.chat.id, "This project is not shared.", message.message_id);
1267
+ return;
1268
+ }
1269
+ const [taskId, memberId] = text.replace("/task assign", "").trim().split(/\s+/, 2);
1270
+ if (!taskId || !memberId) {
1271
+ await this.sendMessage(message.chat.id, "Usage: /task assign <id> <member-id>", message.message_id);
1272
+ return;
1273
+ }
1274
+ try {
1275
+ const result = await assignSharedTask(session.cwd || this.cwd, taskId, memberId, { actorId: userId });
1276
+ await this.sendMessage(message.chat.id, `Assigned shared task <code>${escapeTelegramHtml(result.task.id)}</code> to <code>${escapeTelegramHtml(memberId)}</code>`, message.message_id);
1277
+ } catch (error) {
1278
+ await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
1279
+ }
1280
+ return;
1281
+ }
1282
+
1283
+ if (text.startsWith("/task claim ")) {
1284
+ const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
1285
+ if (!project?.enabled) {
1286
+ await this.sendMessage(message.chat.id, "This project is not shared.", message.message_id);
1287
+ return;
1288
+ }
1289
+ const taskId = text.replace("/task claim", "").trim();
1290
+ if (!taskId) {
1291
+ await this.sendMessage(message.chat.id, "Usage: /task claim <id>", message.message_id);
1292
+ return;
1293
+ }
1294
+ try {
1295
+ const result = await claimSharedTask(session.cwd || this.cwd, taskId, { actorId: userId });
1296
+ await this.sendMessage(message.chat.id, `Claimed shared task <code>${escapeTelegramHtml(result.task.id)}</code>`, message.message_id);
1297
+ } catch (error) {
1298
+ await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
1299
+ }
1300
+ return;
1301
+ }
1302
+
1132
1303
  if (text === "/runtime") {
1133
1304
  const status = await this.runRuntimeStatus();
1134
1305
  await this.sendMessage(message.chat.id, formatRuntimeStatus(status), message.message_id);
@@ -1236,16 +1407,48 @@ class TelegramGateway {
1236
1407
  return;
1237
1408
  }
1238
1409
 
1410
+ const promptText = this.stripBotMention(text);
1411
+ const groupIntent = this.isGroupChat(message) ? classifyTelegramGroupIntent(promptText) : { kind: "execution", reason: "private chat" };
1412
+ const sharedBinding = await this.bindSharedRoomForMessage(message, sessionId);
1413
+ if (
1414
+ this.isGroupChat(message)
1415
+ && sharedBinding.project?.enabled
1416
+ && sharedBinding.project.roomMode === "plan"
1417
+ && /^(task:|todo:)/i.test(promptText)
1418
+ ) {
1419
+ const taskText = promptText.replace(/^(task:|todo:)\s*/i, "").trim();
1420
+ if (taskText) {
1421
+ try {
1422
+ const result = await addSharedTask(sharedBinding.session.cwd || this.cwd, taskText, { actorId: userId });
1423
+ await this.sendMessage(message.chat.id, `Planner captured task <code>${escapeTelegramHtml(result.task.id)}</code>`, message.message_id);
1424
+ } catch (error) {
1425
+ await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
1426
+ }
1427
+ return;
1428
+ }
1429
+ }
1430
+ if (this.isGroupChat(message) && groupIntent.kind !== "execution") {
1431
+ const suggestion = suggestTaskText(promptText);
1432
+ const planHint = groupIntent.kind === "plan"
1433
+ ? `Planning/brainstorming message detected. No execution will run in the group. Suggested task: ${suggestion || "capture the next concrete step"}. Use /task add ${suggestion || "<text>"} or switch to /mode execute when you want Waterbrother to act.`
1434
+ : "Chat message detected. No execution will run in the group. Use /task add to capture work or send an explicit execution request when you want the bot to act.";
1435
+ await this.sendMessage(message.chat.id, escapeTelegramHtml(planHint), message.message_id, { parseMode: "HTML" });
1436
+ return;
1437
+ }
1438
+
1239
1439
  const stopTyping = await this.startTypingLoop(message.chat.id);
1240
1440
  let previewMessage = null;
1241
1441
  try {
1242
1442
  const operatorGate = await this.ensureSharedOperator(message, sessionId);
1243
1443
  if (!operatorGate.ok) {
1244
- await this.sendMessage(message.chat.id, escapeTelegramHtml(operatorGate.reason || "Shared room is not available."), message.message_id, { parseMode: "HTML" });
1444
+ const gateReason =
1445
+ this.isGroupChat(message) && groupIntent.kind === "execution" && operatorGate.project?.enabled && operatorGate.project?.roomMode !== "execute"
1446
+ ? `Execution request detected, but the shared room is in ${operatorGate.project?.roomMode || "chat"} mode. Switch to /mode execute before asking Waterbrother to change code.`
1447
+ : operatorGate.reason || "Shared room is not available.";
1448
+ await this.sendMessage(message.chat.id, escapeTelegramHtml(gateReason), message.message_id, { parseMode: "HTML" });
1245
1449
  return;
1246
1450
  }
1247
1451
  previewMessage = await this.sendProgressMessage(message.chat.id, message.message_id);
1248
- const promptText = this.stripBotMention(text);
1249
1452
  const content = (await this.runPromptViaBridge(message, sessionId, promptText))
1250
1453
  ?? (await this.runPromptFallback(sessionId, promptText));
1251
1454
  await this.deliverPromptResult(message.chat.id, message.message_id, previewMessage, content);
@@ -54,6 +54,7 @@ function normalizeSharedProject(project = {}, cwd = process.cwd()) {
54
54
  roomMode: ["chat", "plan", "execute"].includes(String(project.roomMode || "").trim())
55
55
  ? String(project.roomMode).trim()
56
56
  : "chat",
57
+ runtimeProfile: String(project.runtimeProfile || "").trim(),
57
58
  members,
58
59
  tasks,
59
60
  activeOperator: activeOperator?.id ? activeOperator : null,
@@ -112,7 +113,7 @@ function defaultRoundtableContent(project) {
112
113
  const taskLines = TASK_STATES.flatMap((state) => {
113
114
  const tasks = (project.tasks || []).filter((task) => task.state === state);
114
115
  if (!tasks.length) return [`- ${state}: -`];
115
- return [`- ${state}:`, ...tasks.map((task) => ` - [${task.id}] ${task.text}`)];
116
+ return [`- ${state}:`, ...tasks.map((task) => ` - [${task.id}] ${task.text}${task.assignedTo ? ` @${task.assignedTo}` : ""}`)];
116
117
  });
117
118
  return [
118
119
  "# Roundtable",
@@ -121,6 +122,7 @@ function defaultRoundtableContent(project) {
121
122
  `- Name: ${project.projectName}`,
122
123
  `- Mode: ${project.mode}`,
123
124
  `- Room mode: ${project.roomMode}`,
125
+ `- Runtime profile: ${project.runtimeProfile || "-"}`,
124
126
  `- Approval policy: ${project.approvalPolicy}`,
125
127
  project.room?.provider ? `- Room: ${project.room.provider} ${project.room.chatId || ""}`.trim() : "- Room: not linked",
126
128
  "",
@@ -149,7 +151,7 @@ function buildRoundtableTaskSection(project) {
149
151
  const taskLines = TASK_STATES.flatMap((state) => {
150
152
  const tasks = (project.tasks || []).filter((task) => task.state === state);
151
153
  if (!tasks.length) return [`- ${state}: -`];
152
- return [`- ${state}:`, ...tasks.map((task) => ` - [${task.id}] ${task.text}`)];
154
+ return [`- ${state}:`, ...tasks.map((task) => ` - [${task.id}] ${task.text}${task.assignedTo ? ` @${task.assignedTo}` : ""}`)];
153
155
  });
154
156
  return ["## Task Queue", ...taskLines, ""].join("\n");
155
157
  }
@@ -269,6 +271,21 @@ export async function setSharedRoomMode(cwd, roomMode = "chat", options = {}) {
269
271
  return next;
270
272
  }
271
273
 
274
+ export async function setSharedRuntimeProfile(cwd, runtimeProfile = "", options = {}) {
275
+ const existing = await loadSharedProject(cwd);
276
+ requireOwner(existing, options.actorId);
277
+ const normalized = String(runtimeProfile || "").trim();
278
+ const next = await saveSharedProject(cwd, {
279
+ ...existing,
280
+ runtimeProfile: normalized
281
+ });
282
+ await appendRoundtableEvent(
283
+ cwd,
284
+ `- ${new Date().toISOString()}: room runtime profile ${normalized ? `set to ${normalized}` : "cleared"}`
285
+ );
286
+ return next;
287
+ }
288
+
272
289
  export function getSharedMember(project, memberId = "") {
273
290
  const normalizedId = String(memberId || "").trim();
274
291
  if (!normalizedId || !project?.members?.length) return null;
@@ -409,6 +426,55 @@ export async function moveSharedTask(cwd, taskId = "", state = "open", options =
409
426
  return { project: next, task: tasks[index] };
410
427
  }
411
428
 
429
+ export async function assignSharedTask(cwd, taskId = "", memberId = "", options = {}) {
430
+ const existing = await loadSharedProject(cwd);
431
+ requireEditor(existing, options.actorId);
432
+ const normalizedId = String(taskId || "").trim();
433
+ const normalizedMemberId = String(memberId || "").trim();
434
+ if (!normalizedId) throw new Error("task id is required");
435
+ if (!normalizedMemberId) throw new Error("member id is required");
436
+ if (!getSharedMember(existing, normalizedMemberId)) {
437
+ throw new Error(`No shared-project member found for ${normalizedMemberId}`);
438
+ }
439
+ const tasks = [...(existing.tasks || [])];
440
+ const index = tasks.findIndex((task) => task.id === normalizedId);
441
+ if (index < 0) throw new Error(`No shared task found for ${normalizedId}`);
442
+ tasks[index] = {
443
+ ...tasks[index],
444
+ assignedTo: normalizedMemberId,
445
+ updatedAt: new Date().toISOString()
446
+ };
447
+ const next = await saveSharedProject(cwd, {
448
+ ...existing,
449
+ tasks
450
+ });
451
+ await appendRoundtableEvent(cwd, `- ${new Date().toISOString()}: task assigned [${tasks[index].id}] -> ${normalizedMemberId}`);
452
+ return { project: next, task: tasks[index] };
453
+ }
454
+
455
+ export async function claimSharedTask(cwd, taskId = "", options = {}) {
456
+ const existing = await loadSharedProject(cwd);
457
+ requireMember(existing, options.actorId);
458
+ const normalizedId = String(taskId || "").trim();
459
+ const actorId = String(options.actorId || "").trim();
460
+ if (!normalizedId) throw new Error("task id is required");
461
+ const tasks = [...(existing.tasks || [])];
462
+ const index = tasks.findIndex((task) => task.id === normalizedId);
463
+ if (index < 0) throw new Error(`No shared task found for ${normalizedId}`);
464
+ tasks[index] = {
465
+ ...tasks[index],
466
+ assignedTo: actorId,
467
+ state: "active",
468
+ updatedAt: new Date().toISOString()
469
+ };
470
+ const next = await saveSharedProject(cwd, {
471
+ ...existing,
472
+ tasks
473
+ });
474
+ await appendRoundtableEvent(cwd, `- ${new Date().toISOString()}: task claimed [${tasks[index].id}] by ${actorId}`);
475
+ return { project: next, task: tasks[index] };
476
+ }
477
+
412
478
  export async function claimSharedOperator(cwd, operator = {}) {
413
479
  const existing = await loadSharedProject(cwd);
414
480
  requireSharedProject(existing);