@tritard/waterbrother 0.16.26 → 0.16.28

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
@@ -266,6 +266,7 @@ Shared project foundation is now live:
266
266
  - inspect it with `waterbrother room status`
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
+ - manage the shared backlog with `waterbrother room tasks`, `waterbrother room task add`, and `waterbrother room task move`
269
270
  - claim or release the shared operator lock with `waterbrother room claim` and `waterbrother room release`
270
271
  - shared project metadata lives in `.waterbrother/shared.json`
271
272
  - human collaboration notes live in `ROUNDTABLE.md`
@@ -278,7 +279,7 @@ Current Telegram behavior:
278
279
  - pending pairings are explicit and expire automatically after 12 hours unless approved
279
280
  - paired Telegram users drive the same live session and permissions as the terminal operator when the TUI bridge is attached
280
281
  - Telegram now supports remote workspace control with `/cwd`, `/use <path>`, `/desktop`, and `/new-project <name>`
281
- - shared projects now support `/room`, `/members`, `/mode`, `/claim`, `/release`, `/invite`, and `/remove-member` from Telegram
282
+ - shared projects now support `/room`, `/members`, `/tasks`, `/mode`, `/claim`, `/release`, `/invite`, `/remove-member`, and `/task ...` from Telegram
282
283
  - shared Telegram execution only runs when the shared room is in `execute` mode
283
284
  - room administration is owner-only, and only owners/editors can hold the operator lock
284
285
  - in Telegram groups, Waterbrother only responds when directly targeted: slash commands, `@botname` mentions, or replies to a bot message
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.26",
3
+ "version": "0.16.28",
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
@@ -58,13 +58,16 @@ import { scanForInitiatives, formatInitiatives, buildInitiativeFixPrompt } from
58
58
  import { formatPlanForDisplay } from "./planner.js";
59
59
  import { parseCharterFromGoal, runExperimentLoop, formatExperimentSummary, gitReturnToBranch } from "./experiment.js";
60
60
  import {
61
+ addSharedTask,
61
62
  claimSharedOperator,
62
63
  disableSharedProject,
63
64
  enableSharedProject,
64
65
  formatSharedProjectStatus,
65
66
  getSharedProjectPaths,
66
67
  listSharedMembers,
68
+ listSharedTasks,
67
69
  loadSharedProject,
70
+ moveSharedTask,
68
71
  releaseSharedOperator,
69
72
  removeSharedMember,
70
73
  setSharedRoomMode,
@@ -154,6 +157,9 @@ const INTERACTIVE_COMMANDS = [
154
157
  { name: "/room members", description: "List shared-project members" },
155
158
  { name: "/room add <id> [owner|editor|observer]", description: "Add or update a shared-project member" },
156
159
  { name: "/room remove <id>", description: "Remove a shared-project member" },
160
+ { name: "/room tasks", description: "List Roundtable tasks for the shared project" },
161
+ { name: "/room task add <text>", description: "Add a Roundtable task" },
162
+ { name: "/room task move <id> <open|active|blocked|done>", description: "Move a Roundtable task between states" },
157
163
  { name: "/room mode <chat|plan|execute>", description: "Set collaboration mode for the shared room" },
158
164
  { name: "/room claim", description: "Claim operator control for the shared room" },
159
165
  { name: "/room release", description: "Release operator control for the shared room" },
@@ -281,6 +287,9 @@ Usage:
281
287
  waterbrother room members
282
288
  waterbrother room add <member-id> [owner|editor|observer]
283
289
  waterbrother room remove <member-id>
290
+ waterbrother room tasks
291
+ waterbrother room task add <text>
292
+ waterbrother room task move <id> <open|active|blocked|done>
284
293
  waterbrother room mode <chat|plan|execute>
285
294
  waterbrother room claim
286
295
  waterbrother room release
@@ -3794,6 +3803,54 @@ async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false
3794
3803
  return;
3795
3804
  }
3796
3805
 
3806
+ if (sub === "tasks") {
3807
+ const tasks = await listSharedTasks(cwd);
3808
+ if (asJson) {
3809
+ printData({ ok: true, tasks }, true);
3810
+ return;
3811
+ }
3812
+ if (!tasks.length) {
3813
+ console.log("No shared-project tasks");
3814
+ return;
3815
+ }
3816
+ for (const task of tasks) {
3817
+ console.log(`${task.id}\t${task.state}\t${task.text}`);
3818
+ }
3819
+ return;
3820
+ }
3821
+
3822
+ if (sub === "task") {
3823
+ const action = String(positional[2] || "").trim().toLowerCase();
3824
+ if (action === "add") {
3825
+ const text = String(positional.slice(3).join(" ") || "").trim();
3826
+ if (!text) {
3827
+ throw new Error("Usage: waterbrother room task add <text>");
3828
+ }
3829
+ const result = await addSharedTask(cwd, text, { actorId: operator.id });
3830
+ if (asJson) {
3831
+ printData({ ok: true, action: "task-add", task: result.task, project: result.project }, true);
3832
+ return;
3833
+ }
3834
+ console.log(`Added shared task [${result.task.id}] ${result.task.text}`);
3835
+ return;
3836
+ }
3837
+ if (action === "move") {
3838
+ const taskId = String(positional[3] || "").trim();
3839
+ const state = String(positional[4] || "").trim().toLowerCase();
3840
+ if (!taskId || !state) {
3841
+ throw new Error("Usage: waterbrother room task move <id> <open|active|blocked|done>");
3842
+ }
3843
+ const result = await moveSharedTask(cwd, taskId, state, { actorId: operator.id });
3844
+ if (asJson) {
3845
+ printData({ ok: true, action: "task-move", task: result.task, project: result.project }, true);
3846
+ return;
3847
+ }
3848
+ console.log(`Moved shared task [${result.task.id}] to ${result.task.state}`);
3849
+ return;
3850
+ }
3851
+ throw new Error("Usage: waterbrother room task add <text>|move <id> <open|active|blocked|done>");
3852
+ }
3853
+
3797
3854
  if (sub === "add") {
3798
3855
  const memberId = String(positional[2] || "").trim();
3799
3856
  const role = String(positional[3] || "editor").trim().toLowerCase();
@@ -3868,7 +3925,7 @@ async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false
3868
3925
  return;
3869
3926
  }
3870
3927
 
3871
- throw new Error("Usage: waterbrother room status|members|add <member-id> [owner|editor|observer] [display name]|remove <member-id>|mode <chat|plan|execute>|claim|release");
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");
3872
3929
  }
3873
3930
 
3874
3931
  async function runMcpCommand(positional, runtime, { cwd, asJson = false } = {}) {
@@ -7705,6 +7762,15 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
7705
7762
  continue;
7706
7763
  }
7707
7764
 
7765
+ if (line === "/room tasks") {
7766
+ try {
7767
+ await runRoomCommand(["room", "tasks"], { cwd: context.cwd, asJson: false });
7768
+ } catch (error) {
7769
+ console.log(`room tasks failed: ${error instanceof Error ? error.message : String(error)}`);
7770
+ }
7771
+ continue;
7772
+ }
7773
+
7708
7774
  if (line.startsWith("/room add ")) {
7709
7775
  const raw = line.replace("/room add", "").trim();
7710
7776
  if (!raw) {
@@ -7742,6 +7808,35 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
7742
7808
  continue;
7743
7809
  }
7744
7810
 
7811
+ if (line.startsWith("/room task add ")) {
7812
+ const text = line.replace("/room task add", "").trim();
7813
+ if (!text) {
7814
+ console.log("Usage: /room task add <text>");
7815
+ continue;
7816
+ }
7817
+ try {
7818
+ await runRoomCommand(["room", "task", "add", ...text.split(" ")], { cwd: context.cwd, asJson: false });
7819
+ } catch (error) {
7820
+ console.log(`room task add failed: ${error instanceof Error ? error.message : String(error)}`);
7821
+ }
7822
+ continue;
7823
+ }
7824
+
7825
+ if (line.startsWith("/room task move ")) {
7826
+ const raw = line.replace("/room task move", "").trim();
7827
+ const [taskId, state] = raw.split(/\s+/, 2);
7828
+ if (!taskId || !state) {
7829
+ console.log("Usage: /room task move <id> <open|active|blocked|done>");
7830
+ continue;
7831
+ }
7832
+ try {
7833
+ await runRoomCommand(["room", "task", "move", taskId, state], { cwd: context.cwd, asJson: false });
7834
+ } catch (error) {
7835
+ console.log(`room task move failed: ${error instanceof Error ? error.message : String(error)}`);
7836
+ }
7837
+ continue;
7838
+ }
7839
+
7745
7840
  if (line.startsWith("/room mode ")) {
7746
7841
  const nextMode = line.replace("/room mode", "").trim().toLowerCase();
7747
7842
  if (!nextMode) {
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, getSharedMember, loadSharedProject, releaseSharedOperator, setSharedRoom, setSharedRoomMode, upsertSharedMember, removeSharedMember } from "./shared-project.js";
11
+ import { addSharedTask, claimSharedOperator, getSharedMember, listSharedTasks, loadSharedProject, moveSharedTask, releaseSharedOperator, setSharedRoom, setSharedRoomMode, 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)), "..");
@@ -27,6 +27,7 @@ const TELEGRAM_COMMANDS = [
27
27
  { command: "runtime", description: "Show active runtime status" },
28
28
  { command: "room", description: "Show shared room status" },
29
29
  { command: "members", description: "List shared room members" },
30
+ { command: "tasks", description: "List shared Roundtable tasks" },
30
31
  { command: "mode", description: "Show or set shared room mode" },
31
32
  { command: "claim", description: "Claim operator control for a shared project" },
32
33
  { command: "release", description: "Release operator control for a shared project" },
@@ -201,6 +202,9 @@ function buildRemoteHelp() {
201
202
  "<code>/runtime</code> show active provider/model/runtime state",
202
203
  "<code>/room</code> show shared project room status",
203
204
  "<code>/members</code> list shared project members",
205
+ "<code>/tasks</code> list shared project tasks",
206
+ "<code>/task add &lt;text&gt;</code> add a shared Roundtable task",
207
+ "<code>/task move &lt;id&gt; &lt;open|active|blocked|done&gt;</code> move a shared Roundtable task",
204
208
  "<code>/invite &lt;user-id&gt; [owner|editor|observer]</code> add or update a shared project member",
205
209
  "<code>/remove-member &lt;user-id&gt;</code> remove a shared project member",
206
210
  "<code>/mode</code> or <code>/mode &lt;chat|plan|execute&gt;</code> inspect or change shared room mode",
@@ -310,6 +314,16 @@ function formatTelegramMembersMarkup(project) {
310
314
  ].join("\n");
311
315
  }
312
316
 
317
+ function formatTelegramTasksMarkup(tasks = []) {
318
+ if (!tasks.length) {
319
+ return "<b>Shared tasks</b>\n• none";
320
+ }
321
+ return [
322
+ "<b>Shared tasks</b>",
323
+ ...tasks.map((task) => `• <code>${escapeTelegramHtml(task.id)}</code> <i>(${escapeTelegramHtml(task.state)})</i> ${escapeTelegramHtml(task.text)}`)
324
+ ].join("\n");
325
+ }
326
+
313
327
  function parseInviteCommand(text) {
314
328
  const parts = String(text || "").trim().split(/\s+/).filter(Boolean);
315
329
  const userId = String(parts[1] || "").trim();
@@ -950,6 +964,17 @@ class TelegramGateway {
950
964
  return;
951
965
  }
952
966
 
967
+ if (text === "/tasks") {
968
+ const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
969
+ if (!project?.enabled) {
970
+ await this.sendMessage(message.chat.id, "This project is not shared.", message.message_id);
971
+ return;
972
+ }
973
+ const tasks = await listSharedTasks(session.cwd || this.cwd);
974
+ await this.sendMessage(message.chat.id, formatTelegramTasksMarkup(tasks), message.message_id);
975
+ return;
976
+ }
977
+
953
978
  if (text === "/mode") {
954
979
  const { project } = await this.bindSharedRoomForMessage(message, sessionId);
955
980
  await this.sendMessage(
@@ -1064,6 +1089,46 @@ class TelegramGateway {
1064
1089
  return;
1065
1090
  }
1066
1091
 
1092
+ if (text.startsWith("/task add ")) {
1093
+ const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
1094
+ if (!project?.enabled) {
1095
+ await this.sendMessage(message.chat.id, "This project is not shared.", message.message_id);
1096
+ return;
1097
+ }
1098
+ const taskText = text.replace("/task add", "").trim();
1099
+ if (!taskText) {
1100
+ await this.sendMessage(message.chat.id, "Usage: /task add <text>", message.message_id);
1101
+ return;
1102
+ }
1103
+ try {
1104
+ const result = await addSharedTask(session.cwd || this.cwd, taskText, { actorId: userId });
1105
+ await this.sendMessage(message.chat.id, `Added shared task <code>${escapeTelegramHtml(result.task.id)}</code>`, message.message_id);
1106
+ } catch (error) {
1107
+ await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
1108
+ }
1109
+ return;
1110
+ }
1111
+
1112
+ if (text.startsWith("/task move ")) {
1113
+ const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
1114
+ if (!project?.enabled) {
1115
+ await this.sendMessage(message.chat.id, "This project is not shared.", message.message_id);
1116
+ return;
1117
+ }
1118
+ const [taskId, state] = text.replace("/task move", "").trim().split(/\s+/, 2);
1119
+ if (!taskId || !state) {
1120
+ await this.sendMessage(message.chat.id, "Usage: /task move <id> <open|active|blocked|done>", message.message_id);
1121
+ return;
1122
+ }
1123
+ try {
1124
+ const result = await moveSharedTask(session.cwd || this.cwd, taskId, state, { actorId: userId });
1125
+ await this.sendMessage(message.chat.id, `Moved shared task <code>${escapeTelegramHtml(result.task.id)}</code> to <code>${escapeTelegramHtml(result.task.state)}</code>`, message.message_id);
1126
+ } catch (error) {
1127
+ await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
1128
+ }
1129
+ return;
1130
+ }
1131
+
1067
1132
  if (text === "/runtime") {
1068
1133
  const status = await this.runRuntimeStatus();
1069
1134
  await this.sendMessage(message.chat.id, formatRuntimeStatus(status), message.message_id);
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
 
6
6
  const SHARED_FILE = path.join(".waterbrother", "shared.json");
7
7
  const ROUNDTABLE_FILE = "ROUNDTABLE.md";
8
+ const TASK_STATES = ["open", "active", "blocked", "done"];
8
9
 
9
10
  function normalizeMember(member = {}) {
10
11
  return {
@@ -25,6 +26,11 @@ function memberRoleWeight(role = "") {
25
26
 
26
27
  function normalizeSharedProject(project = {}, cwd = process.cwd()) {
27
28
  const members = Array.isArray(project.members) ? project.members.map(normalizeMember).filter((item) => item.id) : [];
29
+ const tasks = Array.isArray(project.tasks)
30
+ ? project.tasks
31
+ .map((task) => normalizeSharedTask(task))
32
+ .filter((task) => task.id && task.text)
33
+ : [];
28
34
  const activeOperator = project.activeOperator && typeof project.activeOperator === "object"
29
35
  ? {
30
36
  id: String(project.activeOperator.id || "").trim(),
@@ -49,6 +55,7 @@ function normalizeSharedProject(project = {}, cwd = process.cwd()) {
49
55
  ? String(project.roomMode).trim()
50
56
  : "chat",
51
57
  members,
58
+ tasks,
52
59
  activeOperator: activeOperator?.id ? activeOperator : null,
53
60
  approvalPolicy: String(project.approvalPolicy || "owner").trim() || "owner",
54
61
  createdAt: String(project.createdAt || new Date().toISOString()).trim(),
@@ -56,6 +63,18 @@ function normalizeSharedProject(project = {}, cwd = process.cwd()) {
56
63
  };
57
64
  }
58
65
 
66
+ function normalizeSharedTask(task = {}) {
67
+ return {
68
+ id: String(task.id || `rt_${crypto.randomBytes(3).toString("hex")}`).trim(),
69
+ text: String(task.text || "").trim(),
70
+ state: TASK_STATES.includes(String(task.state || "").trim()) ? String(task.state).trim() : "open",
71
+ createdAt: String(task.createdAt || new Date().toISOString()).trim(),
72
+ updatedAt: String(task.updatedAt || new Date().toISOString()).trim(),
73
+ createdBy: String(task.createdBy || "").trim(),
74
+ assignedTo: String(task.assignedTo || "").trim()
75
+ };
76
+ }
77
+
59
78
  function sharedFilePath(cwd) {
60
79
  return path.join(cwd, SHARED_FILE);
61
80
  }
@@ -90,6 +109,11 @@ function defaultRoundtableContent(project) {
90
109
  const activeOperator = project.activeOperator?.id
91
110
  ? `${project.activeOperator.name || project.activeOperator.id}`
92
111
  : "none";
112
+ const taskLines = TASK_STATES.flatMap((state) => {
113
+ const tasks = (project.tasks || []).filter((task) => task.state === state);
114
+ if (!tasks.length) return [`- ${state}: -`];
115
+ return [`- ${state}:`, ...tasks.map((task) => ` - [${task.id}] ${task.text}`)];
116
+ });
93
117
  return [
94
118
  "# Roundtable",
95
119
  "",
@@ -116,14 +140,35 @@ function defaultRoundtableContent(project) {
116
140
  "-",
117
141
  "",
118
142
  "## Task Queue",
119
- "- open: -",
120
- "- active: -",
121
- "- blocked: -",
122
- "- done: -",
143
+ ...taskLines,
123
144
  ""
124
145
  ].join("\n");
125
146
  }
126
147
 
148
+ function buildRoundtableTaskSection(project) {
149
+ const taskLines = TASK_STATES.flatMap((state) => {
150
+ const tasks = (project.tasks || []).filter((task) => task.state === state);
151
+ if (!tasks.length) return [`- ${state}: -`];
152
+ return [`- ${state}:`, ...tasks.map((task) => ` - [${task.id}] ${task.text}`)];
153
+ });
154
+ return ["## Task Queue", ...taskLines, ""].join("\n");
155
+ }
156
+
157
+ async function syncRoundtableTaskSection(cwd, project) {
158
+ const target = roundtablePath(cwd);
159
+ if (!(await pathExists(target))) return null;
160
+ const current = await fs.readFile(target, "utf8");
161
+ const nextSection = buildRoundtableTaskSection(project);
162
+ let nextContent;
163
+ if (/^## Task Queue\s*$/m.test(current)) {
164
+ nextContent = current.replace(/## Task Queue[\s\S]*$/, nextSection);
165
+ } else {
166
+ nextContent = `${current.trimEnd()}\n\n${nextSection}`;
167
+ }
168
+ await fs.writeFile(target, nextContent.endsWith("\n") ? nextContent : `${nextContent}\n`, "utf8");
169
+ return target;
170
+ }
171
+
127
172
  export async function ensureRoundtable(cwd, project) {
128
173
  const target = roundtablePath(cwd);
129
174
  if (await pathExists(target)) return target;
@@ -156,6 +201,7 @@ export async function saveSharedProject(cwd, project) {
156
201
  }, cwd);
157
202
  await writeJsonAtomically(sharedFilePath(cwd), next);
158
203
  await ensureRoundtable(cwd, next);
204
+ await syncRoundtableTaskSection(cwd, next);
159
205
  return next;
160
206
  }
161
207
 
@@ -247,6 +293,20 @@ function requireOwner(project, actorId = "") {
247
293
  }
248
294
  }
249
295
 
296
+ function requireEditor(project, actorId = "") {
297
+ requireSharedProject(project);
298
+ if (!memberHasAtLeastRole(project, actorId, "editor")) {
299
+ throw new Error("Only a shared-project owner or editor can do that.");
300
+ }
301
+ }
302
+
303
+ function requireMember(project, actorId = "") {
304
+ requireSharedProject(project);
305
+ if (!getSharedMember(project, actorId)) {
306
+ throw new Error("Only a shared-project member can do that.");
307
+ }
308
+ }
309
+
250
310
  export async function listSharedMembers(cwd) {
251
311
  const project = await loadSharedProject(cwd);
252
312
  requireSharedProject(project);
@@ -299,6 +359,56 @@ export async function removeSharedMember(cwd, memberId = "", options = {}) {
299
359
  return next;
300
360
  }
301
361
 
362
+ export async function listSharedTasks(cwd) {
363
+ const project = await loadSharedProject(cwd);
364
+ requireSharedProject(project);
365
+ return project.tasks || [];
366
+ }
367
+
368
+ export async function addSharedTask(cwd, text = "", options = {}) {
369
+ const existing = await loadSharedProject(cwd);
370
+ requireMember(existing, options.actorId);
371
+ const normalizedText = String(text || "").trim();
372
+ if (!normalizedText) throw new Error("task text is required");
373
+ const task = normalizeSharedTask({
374
+ text: normalizedText,
375
+ state: "open",
376
+ createdBy: String(options.actorId || "").trim()
377
+ });
378
+ const next = await saveSharedProject(cwd, {
379
+ ...existing,
380
+ tasks: [...(existing.tasks || []), task]
381
+ });
382
+ await appendRoundtableEvent(cwd, `- ${new Date().toISOString()}: task added [${task.id}] ${task.text}`);
383
+ return { project: next, task };
384
+ }
385
+
386
+ export async function moveSharedTask(cwd, taskId = "", state = "open", options = {}) {
387
+ const existing = await loadSharedProject(cwd);
388
+ requireEditor(existing, options.actorId);
389
+ const normalizedId = String(taskId || "").trim();
390
+ const normalizedState = String(state || "").trim().toLowerCase();
391
+ if (!normalizedId) throw new Error("task id is required");
392
+ if (!TASK_STATES.includes(normalizedState)) {
393
+ throw new Error(`Invalid task state. Expected one of ${TASK_STATES.join(", ")}.`);
394
+ }
395
+ const tasks = [...(existing.tasks || [])];
396
+ const index = tasks.findIndex((task) => task.id === normalizedId);
397
+ if (index < 0) throw new Error(`No shared task found for ${normalizedId}`);
398
+ tasks[index] = {
399
+ ...tasks[index],
400
+ state: normalizedState,
401
+ updatedAt: new Date().toISOString(),
402
+ assignedTo: normalizedState === "active" ? String(options.actorId || "").trim() : tasks[index].assignedTo
403
+ };
404
+ const next = await saveSharedProject(cwd, {
405
+ ...existing,
406
+ tasks
407
+ });
408
+ await appendRoundtableEvent(cwd, `- ${new Date().toISOString()}: task moved [${tasks[index].id}] -> ${normalizedState}`);
409
+ return { project: next, task: tasks[index] };
410
+ }
411
+
302
412
  export async function claimSharedOperator(cwd, operator = {}) {
303
413
  const existing = await loadSharedProject(cwd);
304
414
  requireSharedProject(existing);
@@ -361,7 +471,8 @@ export function formatSharedProjectStatus(project) {
361
471
  roomMode: project.roomMode,
362
472
  approvalPolicy: project.approvalPolicy,
363
473
  activeOperator: project.activeOperator,
364
- members: project.members
474
+ members: project.members,
475
+ tasks: project.tasks
365
476
  }, null, 2);
366
477
  }
367
478