@tritard/waterbrother 0.16.39 → 0.16.40

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
@@ -297,14 +297,15 @@ Current Telegram behavior:
297
297
  - Telegram now supports remote workspace control with `/cwd`, `/use <path>`, `/desktop`, and `/new-project <name>`
298
298
  - shared projects now support `/room`, `/members`, `/invites`, `/tasks`, `/mode`, `/claim`, `/release`, `/invite`, `/accept-invite`, `/approve-invite`, `/reject-invite`, `/remove-member`, `/room-runtime`, and `/task ...` from Telegram
299
299
  - `/room` now includes pending invite count plus task ownership summaries
300
- - local `/status` now includes `sharedRoom` with pending invites, task ownership summary, and recent task activity
300
+ - local `/status` now includes `sharedRoom` with pending invites, task ownership summary, and recent shared-room event activity
301
+ - the TUI now prints a small Roundtable event feed when new shared-room activity lands
301
302
  - Telegram now posts a Roundtable ownership notice when shared task ownership changes via assign/claim/move
302
303
  - shared Telegram execution only runs when the shared room is in `execute` mode
303
304
  - room administration is owner-only, and only owners/editors can hold the operator lock
304
305
  - `/room` status now shows the active executor surface plus provider/model/runtime identity
305
306
  - repo-first concept resolution now covers Waterbrother itself, Roundtable, Telegram, shared rooms, the gateway, runtime profiles, approvals, and sessions
306
307
  - in Telegram groups, Waterbrother only responds when directly targeted: slash commands, `@botname` mentions, or replies to a bot message
307
- - in Telegram groups, directly targeted messages are now classified as chat, planning, or execution; only explicit execution requests run the live session
308
+ - in Telegram groups, directly targeted messages are now classified as chat, planning, or execution; explicit execution should use `/run <prompt>`
308
309
  - in Telegram group `chat` or `plan` flows, targeted Waterbrother/project questions are now answered directly from local repo state instead of returning only a generic planner hint
309
310
  - in shared plan-mode groups, targeted `task:` and `todo:` messages are captured directly into the Roundtable task queue
310
311
  - pairing is now explicit: first DM creates a pending request, then approve locally with `waterbrother gateway pair telegram <user-id>`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.39",
3
+ "version": "0.16.40",
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
@@ -2682,18 +2682,11 @@ function buildRoomStatusPayload(project, runtime = null, currentSession = null)
2682
2682
  const assignee = String(task?.assignedTo || "").trim() || "unassigned";
2683
2683
  taskSummary.byAssignee[assignee] = (taskSummary.byAssignee[assignee] || 0) + 1;
2684
2684
  }
2685
- const recentActivity = tasks
2686
- .flatMap((task) => (Array.isArray(task.history) ? task.history.map((entry) => ({
2687
- taskId: task.id,
2688
- taskText: task.text,
2689
- type: entry.type,
2690
- text: entry.text,
2691
- actorId: entry.actorId || "",
2692
- actorName: entry.actorName || "",
2693
- createdAt: entry.createdAt || ""
2694
- })) : []))
2695
- .sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)))
2696
- .slice(0, 5);
2685
+ const recentActivity = Array.isArray(project.recentEvents)
2686
+ ? [...project.recentEvents]
2687
+ .sort((a, b) => String(b.createdAt || "").localeCompare(String(a.createdAt || "")))
2688
+ .slice(0, 8)
2689
+ : [];
2697
2690
  return {
2698
2691
  ...project,
2699
2692
  pendingInviteCount: Array.isArray(project.pendingInvites) ? project.pendingInvites.length : 0,
@@ -2711,6 +2704,39 @@ function buildRoomStatusPayload(project, runtime = null, currentSession = null)
2711
2704
  };
2712
2705
  }
2713
2706
 
2707
+ function getLatestSharedRoomEventId(project) {
2708
+ const events = Array.isArray(project?.recentEvents) ? project.recentEvents : [];
2709
+ return events.length ? String(events[events.length - 1]?.id || "").trim() : "";
2710
+ }
2711
+
2712
+ function formatSharedRoomEventFeedLine(event = {}) {
2713
+ const timestamp = String(event.createdAt || "").trim();
2714
+ const shortTime = timestamp ? timestamp.replace("T", " ").replace(/\.\d+Z$/, "Z") : "";
2715
+ const actor = String(event.actorName || event.actorId || "").trim();
2716
+ const prefix = actor ? `${actor}: ` : "";
2717
+ return `${styleSystemPrefix()} ${dim("[roundtable]")} ${shortTime ? `${shortTime} ` : ""}${prefix}${event.text || ""}`.trim();
2718
+ }
2719
+
2720
+ async function flushSharedRoomEventFeed({ cwd, lastSeenEventId = "" } = {}) {
2721
+ const project = await loadSharedProject(cwd).catch(() => null);
2722
+ const events = Array.isArray(project?.recentEvents) ? project.recentEvents : [];
2723
+ if (!events.length) {
2724
+ return { latestEventId: "", printed: 0 };
2725
+ }
2726
+ let unseen = events;
2727
+ if (lastSeenEventId) {
2728
+ const index = events.findIndex((event) => String(event.id || "").trim() === lastSeenEventId);
2729
+ unseen = index >= 0 ? events.slice(index + 1) : [];
2730
+ }
2731
+ for (const event of unseen) {
2732
+ console.log(formatSharedRoomEventFeedLine(event));
2733
+ }
2734
+ return {
2735
+ latestEventId: getLatestSharedRoomEventId(project),
2736
+ printed: unseen.length
2737
+ };
2738
+ }
2739
+
2714
2740
  async function applyRuntimeSelection({
2715
2741
  config,
2716
2742
  context,
@@ -3844,11 +3870,64 @@ function getLocalOperatorIdentity() {
3844
3870
  };
3845
3871
  }
3846
3872
 
3873
+ function printSharedProjectNextSteps(project) {
3874
+ console.log(dim(`room mode: ${project.roomMode || "chat"}`));
3875
+ console.log(dim(`room runtime: ${project.runtimeProfile || "none"}`));
3876
+ console.log(dim("Next: invite members with `waterbrother room invite <member-id> [owner|editor|observer]`, then use `waterbrother room mode execute` when you want a claimed operator to act."));
3877
+ }
3878
+
3879
+ async function configureSharedProjectInteractive(cwd, operator, initialProject) {
3880
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
3881
+ return initialProject;
3882
+ }
3883
+ let project = initialProject;
3884
+ const currentMode = project?.roomMode || "chat";
3885
+ const enteredMode = (await promptLine(`Shared room mode [${currentMode}] (chat|plan|execute, Enter to keep): `)).trim().toLowerCase();
3886
+ if (enteredMode) {
3887
+ if (!["chat", "plan", "execute"].includes(enteredMode)) {
3888
+ console.log(yellow("Ignoring invalid room mode. Keeping existing mode."));
3889
+ } else if (enteredMode !== currentMode) {
3890
+ project = await setSharedRoomMode(cwd, enteredMode, { actorId: operator.id });
3891
+ }
3892
+ }
3893
+
3894
+ const currentRuntime = project?.runtimeProfile || "";
3895
+ const enteredRuntime = (await promptLine(`Shared room runtime profile [${currentRuntime || "none"}] (Enter to keep, clear to unset): `)).trim();
3896
+ if (enteredRuntime) {
3897
+ const nextRuntime = enteredRuntime.toLowerCase() === "clear" ? "" : enteredRuntime;
3898
+ project = await setSharedRuntimeProfile(cwd, nextRuntime, { actorId: operator.id });
3899
+ }
3900
+
3901
+ const createInvite = await promptYesNo("Create the first shared-project invite now?", {
3902
+ input: process.stdin,
3903
+ output: process.stdout
3904
+ });
3905
+ if (createInvite) {
3906
+ const memberId = (await promptLine("Telegram user id or member id: ")).trim();
3907
+ if (memberId) {
3908
+ const role = (await promptLine("Role [editor] (owner|editor|observer): ")).trim().toLowerCase() || "editor";
3909
+ const name = (await promptLine("Display name (Enter to reuse id): ")).trim() || memberId;
3910
+ const result = await createSharedInvite(
3911
+ cwd,
3912
+ { id: memberId, role, name, paired: true },
3913
+ { actorId: operator.id, actorName: operator.name }
3914
+ );
3915
+ project = result.project;
3916
+ console.log(green(`Created shared-project invite [${result.invite.id}] for ${memberId} as ${result.invite.role}`));
3917
+ }
3918
+ }
3919
+
3920
+ return project;
3921
+ }
3922
+
3847
3923
  async function runProjectCommand(positional, { cwd = process.cwd(), asJson = false } = {}) {
3848
3924
  const sub = String(positional[1] || "").trim().toLowerCase();
3849
3925
  if (sub === "share") {
3850
3926
  const operator = getLocalOperatorIdentity();
3851
- const project = await enableSharedProject(cwd, { userId: operator.id, userName: operator.name, role: "owner" });
3927
+ let project = await enableSharedProject(cwd, { userId: operator.id, userName: operator.name, role: "owner" });
3928
+ if (!asJson) {
3929
+ project = await configureSharedProjectInteractive(cwd, operator, project);
3930
+ }
3852
3931
  const paths = getSharedProjectPaths(cwd);
3853
3932
  if (asJson) {
3854
3933
  printData({ ok: true, action: "share", project, paths }, true);
@@ -3857,6 +3936,7 @@ async function runProjectCommand(positional, { cwd = process.cwd(), asJson = fal
3857
3936
  console.log(`Shared project enabled in ${cwd}`);
3858
3937
  console.log(`shared metadata: ${paths.sharedJson}`);
3859
3938
  console.log(`roundtable: ${paths.roundtable}`);
3939
+ printSharedProjectNextSteps(project);
3860
3940
  return;
3861
3941
  }
3862
3942
 
@@ -7342,7 +7422,12 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
7342
7422
  console.log(formatBorderRow(clampColumns(process.stdout.columns || 80)));
7343
7423
  }
7344
7424
 
7425
+ let lastSeenSharedRoomEventId = getLatestSharedRoomEventId(await loadSharedProject(context.cwd).catch(() => null));
7345
7426
  while (true) {
7427
+ const sharedFeed = await flushSharedRoomEventFeed({ cwd: context.cwd, lastSeenEventId: lastSeenSharedRoomEventId });
7428
+ if (sharedFeed.latestEventId) {
7429
+ lastSeenSharedRoomEventId = sharedFeed.latestEventId;
7430
+ }
7346
7431
  await touchTelegramBridgeHost({ sessionId: currentSession.id, cwd: context.cwd });
7347
7432
  const nextInput = await readInteractiveLine({
7348
7433
  getFooterText(inputBuffer) {
@@ -7908,6 +7993,16 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
7908
7993
  context.costTracker = createCostTracker();
7909
7994
  console.log(`started new project session: ${currentSession.id}`);
7910
7995
  console.log(`cwd set to ${target}`);
7996
+ const shareProject = await promptYesNo("Make this new project shared?", { input: process.stdin, output: process.stdout });
7997
+ if (shareProject) {
7998
+ const operator = getLocalOperatorIdentity();
7999
+ const project = await configureSharedProjectInteractive(
8000
+ target,
8001
+ operator,
8002
+ await enableSharedProject(target, { userId: operator.id, userName: operator.name, role: "owner" })
8003
+ );
8004
+ printSharedProjectNextSteps(project);
8005
+ }
7911
8006
  } catch (error) {
7912
8007
  console.log(`new project failed: ${error instanceof Error ? error.message : String(error)}`);
7913
8008
  }
package/src/gateway.js CHANGED
@@ -25,6 +25,7 @@ const TELEGRAM_COMMANDS = [
25
25
  { command: "help", description: "Show Telegram control help" },
26
26
  { command: "about", description: "Show local Waterbrother identity and capabilities" },
27
27
  { command: "state", description: "Show current Waterbrother self-awareness state" },
28
+ { command: "run", description: "Execute an explicit remote prompt" },
28
29
  { command: "status", description: "Show the linked remote session" },
29
30
  { command: "cwd", description: "Show the current remote working directory" },
30
31
  { command: "runtime", description: "Show active runtime status" },
@@ -201,6 +202,7 @@ function buildRemoteHelp() {
201
202
  "<code>/help</code> show this help",
202
203
  "<code>/about</code> show Waterbrother identity and local capabilities",
203
204
  "<code>/state</code> show current Waterbrother self-awareness state",
205
+ "<code>/run &lt;prompt&gt;</code> execute an explicit remote request",
204
206
  "<code>/status</code> show the current linked remote session",
205
207
  "<code>/cwd</code> show the current remote working directory",
206
208
  "<code>/use &lt;path&gt;</code> switch the linked session to another directory",
@@ -231,7 +233,8 @@ function buildRemoteHelp() {
231
233
  "<code>/new</code> start a fresh remote session",
232
234
  "<code>/clear</code> clear the current remote conversation history",
233
235
  "",
234
- "Any other message is sent to the linked Waterbrother session.",
236
+ "In private chats, any other message is sent to the linked Waterbrother session.",
237
+ "In groups, explicit execution should use <code>/run &lt;prompt&gt;</code>.",
235
238
  "",
236
239
  "<b>Current limitation</b>",
237
240
  "• fallback remote runs still use <code>approval=never</code> if no live TUI host is attached",
@@ -1662,8 +1665,16 @@ class TelegramGateway {
1662
1665
  return;
1663
1666
  }
1664
1667
 
1665
- const promptText = this.stripBotMention(text);
1666
- const groupIntent = this.isGroupChat(message) ? classifyTelegramGroupIntent(promptText) : { kind: "execution", reason: "private chat" };
1668
+ if (text === "/run") {
1669
+ await this.sendMessage(message.chat.id, "Usage: <code>/run &lt;prompt&gt;</code>", message.message_id);
1670
+ return;
1671
+ }
1672
+
1673
+ const isExplicitRun = text.startsWith("/run ");
1674
+ const promptText = this.stripBotMention(isExplicitRun ? text.replace("/run", "").trim() : text);
1675
+ const groupIntent = this.isGroupChat(message)
1676
+ ? (isExplicitRun ? { kind: "execution", reason: "explicit /run" } : classifyTelegramGroupIntent(promptText))
1677
+ : { kind: "execution", reason: "private chat" };
1667
1678
  const sharedBinding = await this.bindSharedRoomForMessage(message, sessionId);
1668
1679
  const manifest = await buildSelfAwarenessManifest({ cwd: sharedBinding.session.cwd || this.cwd, runtime: this.runtime, currentSession: sharedBinding.session });
1669
1680
  const localConceptAnswer = resolveLocalConceptQuestion(promptText, manifest);
@@ -1696,6 +1707,14 @@ class TelegramGateway {
1696
1707
  await this.sendMessage(message.chat.id, escapeTelegramHtml(planHint), message.message_id, { parseMode: "HTML" });
1697
1708
  return;
1698
1709
  }
1710
+ if (this.isGroupChat(message) && groupIntent.kind === "execution" && !isExplicitRun) {
1711
+ await this.sendMessage(
1712
+ message.chat.id,
1713
+ "Execution requests in groups must use <code>/run &lt;prompt&gt;</code>. Discussion and planning can stay as normal targeted messages.",
1714
+ message.message_id
1715
+ );
1716
+ return;
1717
+ }
1699
1718
  if (localConceptAnswer) {
1700
1719
  await this.sendMessage(message.chat.id, renderTelegramChunks(localConceptAnswer).join("\n\n"), message.message_id, { parseMode: "HTML" });
1701
1720
  return;
@@ -6,6 +6,7 @@ import path from "node:path";
6
6
  const SHARED_FILE = path.join(".waterbrother", "shared.json");
7
7
  const ROUNDTABLE_FILE = "ROUNDTABLE.md";
8
8
  const TASK_STATES = ["open", "active", "blocked", "done"];
9
+ const RECENT_EVENT_LIMIT = 24;
9
10
 
10
11
  function makeId(prefix = "id") {
11
12
  return `${prefix}_${crypto.randomBytes(3).toString("hex")}`;
@@ -40,6 +41,12 @@ function normalizeSharedProject(project = {}, cwd = process.cwd()) {
40
41
  .map((invite) => normalizePendingInvite(invite))
41
42
  .filter((invite) => invite.id && invite.memberId && invite.status === "pending")
42
43
  : [];
44
+ const recentEvents = Array.isArray(project.recentEvents)
45
+ ? project.recentEvents
46
+ .map((event) => normalizeSharedEvent(event))
47
+ .filter((event) => event.id && event.text)
48
+ .slice(-RECENT_EVENT_LIMIT)
49
+ : [];
43
50
  const activeOperator = project.activeOperator && typeof project.activeOperator === "object"
44
51
  ? {
45
52
  id: String(project.activeOperator.id || "").trim(),
@@ -67,6 +74,7 @@ function normalizeSharedProject(project = {}, cwd = process.cwd()) {
67
74
  members,
68
75
  tasks,
69
76
  pendingInvites,
77
+ recentEvents,
70
78
  activeOperator: activeOperator?.id ? activeOperator : null,
71
79
  approvalPolicy: String(project.approvalPolicy || "owner").trim() || "owner",
72
80
  createdAt: String(project.createdAt || new Date().toISOString()).trim(),
@@ -74,6 +82,18 @@ function normalizeSharedProject(project = {}, cwd = process.cwd()) {
74
82
  };
75
83
  }
76
84
 
85
+ function normalizeSharedEvent(event = {}) {
86
+ return {
87
+ id: String(event.id || makeId("rte")).trim(),
88
+ type: String(event.type || "note").trim(),
89
+ text: String(event.text || "").trim(),
90
+ actorId: String(event.actorId || "").trim(),
91
+ actorName: String(event.actorName || "").trim(),
92
+ createdAt: String(event.createdAt || new Date().toISOString()).trim(),
93
+ meta: event.meta && typeof event.meta === "object" ? { ...event.meta } : {}
94
+ };
95
+ }
96
+
77
97
  function normalizeSharedTask(task = {}) {
78
98
  const comments = Array.isArray(task.comments)
79
99
  ? task.comments
@@ -269,6 +289,16 @@ export async function appendRoundtableEvent(cwd, line) {
269
289
  return target;
270
290
  }
271
291
 
292
+ async function recordSharedProjectEvent(cwd, project, text, { type = "note", actorId = "", actorName = "", meta = {} } = {}) {
293
+ const event = normalizeSharedEvent({ type, text, actorId, actorName, meta });
294
+ const next = await saveSharedProject(cwd, {
295
+ ...project,
296
+ recentEvents: [...(project.recentEvents || []), event].slice(-RECENT_EVENT_LIMIT)
297
+ });
298
+ await appendRoundtableEvent(cwd, `- ${event.createdAt}: ${event.text}`);
299
+ return { project: next, event };
300
+ }
301
+
272
302
  export async function loadSharedProject(cwd) {
273
303
  try {
274
304
  const raw = await fs.readFile(sharedFilePath(cwd), "utf8");
@@ -311,8 +341,11 @@ export async function enableSharedProject(cwd, options = {}) {
311
341
  claimedAt: new Date().toISOString()
312
342
  }
313
343
  });
314
- await appendRoundtableEvent(cwd, `- ${new Date().toISOString()}: sharing enabled by ${member.name || member.id}`);
315
- return project;
344
+ return (await recordSharedProjectEvent(cwd, project, `sharing enabled by ${member.name || member.id}`, {
345
+ type: "sharing-enabled",
346
+ actorId: member.id,
347
+ actorName: member.name
348
+ })).project;
316
349
  }
317
350
 
318
351
  export async function disableSharedProject(cwd) {
@@ -323,8 +356,7 @@ export async function disableSharedProject(cwd) {
323
356
  enabled: false,
324
357
  activeOperator: null
325
358
  });
326
- await appendRoundtableEvent(cwd, `- ${new Date().toISOString()}: sharing disabled`);
327
- return next;
359
+ return (await recordSharedProjectEvent(cwd, next, "sharing disabled", { type: "sharing-disabled" })).project;
328
360
  }
329
361
 
330
362
  export async function setSharedRoom(cwd, room = {}) {
@@ -338,7 +370,9 @@ export async function setSharedRoom(cwd, room = {}) {
338
370
  title: String(room.title || existing.room?.title || "").trim()
339
371
  }
340
372
  });
341
- return next;
373
+ return (await recordSharedProjectEvent(cwd, next, `room linked to ${next.room.provider || "unknown"} ${next.room.chatId || ""}`.trim(), {
374
+ type: "room-linked"
375
+ })).project;
342
376
  }
343
377
 
344
378
  export async function setSharedRoomMode(cwd, roomMode = "chat", options = {}) {
@@ -352,8 +386,10 @@ export async function setSharedRoomMode(cwd, roomMode = "chat", options = {}) {
352
386
  ...existing,
353
387
  roomMode: normalized
354
388
  });
355
- await appendRoundtableEvent(cwd, `- ${new Date().toISOString()}: room mode set to ${normalized}`);
356
- return next;
389
+ return (await recordSharedProjectEvent(cwd, next, `room mode set to ${normalized}`, {
390
+ type: "room-mode",
391
+ actorId: String(options.actorId || "").trim()
392
+ })).project;
357
393
  }
358
394
 
359
395
  export async function setSharedRuntimeProfile(cwd, runtimeProfile = "", options = {}) {
@@ -364,11 +400,10 @@ export async function setSharedRuntimeProfile(cwd, runtimeProfile = "", options
364
400
  ...existing,
365
401
  runtimeProfile: normalized
366
402
  });
367
- await appendRoundtableEvent(
368
- cwd,
369
- `- ${new Date().toISOString()}: room runtime profile ${normalized ? `set to ${normalized}` : "cleared"}`
370
- );
371
- return next;
403
+ return (await recordSharedProjectEvent(cwd, next, `room runtime profile ${normalized ? `set to ${normalized}` : "cleared"}`, {
404
+ type: "room-runtime",
405
+ actorId: String(options.actorId || "").trim()
406
+ })).project;
372
407
  }
373
408
 
374
409
  export function getSharedMember(project, memberId = "") {
@@ -437,11 +472,11 @@ export async function upsertSharedMember(cwd, member = {}, options = {}) {
437
472
  ...existing,
438
473
  members
439
474
  });
440
- await appendRoundtableEvent(
441
- cwd,
442
- `- ${new Date().toISOString()}: member ${nextMember.name || nextMember.id} set to role ${nextMember.role}`
443
- );
444
- return next;
475
+ return (await recordSharedProjectEvent(cwd, next, `member ${nextMember.name || nextMember.id} set to role ${nextMember.role}`, {
476
+ type: "member-upsert",
477
+ actorId: String(options.actorId || "").trim(),
478
+ meta: { memberId: nextMember.id, role: nextMember.role }
479
+ })).project;
445
480
  }
446
481
 
447
482
  export async function createSharedInvite(cwd, member = {}, options = {}) {
@@ -468,11 +503,13 @@ export async function createSharedInvite(cwd, member = {}, options = {}) {
468
503
  ...existing,
469
504
  pendingInvites
470
505
  });
471
- await appendRoundtableEvent(
472
- cwd,
473
- `- ${new Date().toISOString()}: invite created [${invite.id}] for ${invite.memberName || invite.memberId} as ${invite.role}`
474
- );
475
- return { project: next, invite };
506
+ const recorded = await recordSharedProjectEvent(cwd, next, `invite created [${invite.id}] for ${invite.memberName || invite.memberId} as ${invite.role}`, {
507
+ type: "invite-created",
508
+ actorId: String(options.actorId || "").trim(),
509
+ actorName: String(options.actorName || "").trim(),
510
+ meta: { inviteId: invite.id, memberId: invite.memberId, role: invite.role }
511
+ });
512
+ return { project: recorded.project, invite };
476
513
  }
477
514
 
478
515
  export async function approveSharedInvite(cwd, inviteId = "", options = {}) {
@@ -499,11 +536,12 @@ export async function approveSharedInvite(cwd, inviteId = "", options = {}) {
499
536
  members,
500
537
  pendingInvites
501
538
  });
502
- await appendRoundtableEvent(
503
- cwd,
504
- `- ${new Date().toISOString()}: invite approved [${invite.id}] for ${invite.memberName || invite.memberId}`
505
- );
506
- return { project: next, invite };
539
+ const recorded = await recordSharedProjectEvent(cwd, next, `invite approved [${invite.id}] for ${invite.memberName || invite.memberId}`, {
540
+ type: "invite-approved",
541
+ actorId: String(options.actorId || "").trim(),
542
+ meta: { inviteId: invite.id, memberId: invite.memberId }
543
+ });
544
+ return { project: recorded.project, invite };
507
545
  }
508
546
 
509
547
  export async function rejectSharedInvite(cwd, inviteId = "", options = {}) {
@@ -519,11 +557,12 @@ export async function rejectSharedInvite(cwd, inviteId = "", options = {}) {
519
557
  ...existing,
520
558
  pendingInvites
521
559
  });
522
- await appendRoundtableEvent(
523
- cwd,
524
- `- ${new Date().toISOString()}: invite rejected [${invite.id}] for ${invite.memberName || invite.memberId}`
525
- );
526
- return { project: next, invite };
560
+ const recorded = await recordSharedProjectEvent(cwd, next, `invite rejected [${invite.id}] for ${invite.memberName || invite.memberId}`, {
561
+ type: "invite-rejected",
562
+ actorId: String(options.actorId || "").trim(),
563
+ meta: { inviteId: invite.id, memberId: invite.memberId }
564
+ });
565
+ return { project: recorded.project, invite };
527
566
  }
528
567
 
529
568
  export async function acceptSharedInvite(cwd, inviteId = "", options = {}) {
@@ -556,11 +595,13 @@ export async function acceptSharedInvite(cwd, inviteId = "", options = {}) {
556
595
  members,
557
596
  pendingInvites
558
597
  });
559
- await appendRoundtableEvent(
560
- cwd,
561
- `- ${new Date().toISOString()}: invite accepted [${invite.id}] by ${actorName || actorId}`
562
- );
563
- return { project: next, invite };
598
+ const recorded = await recordSharedProjectEvent(cwd, next, `invite accepted [${invite.id}] by ${actorName || actorId}`, {
599
+ type: "invite-accepted",
600
+ actorId,
601
+ actorName,
602
+ meta: { inviteId: invite.id, memberId: invite.memberId }
603
+ });
604
+ return { project: recorded.project, invite };
564
605
  }
565
606
 
566
607
  export async function removeSharedMember(cwd, memberId = "", options = {}) {
@@ -579,11 +620,11 @@ export async function removeSharedMember(cwd, memberId = "", options = {}) {
579
620
  members: (existing.members || []).filter((member) => member.id !== normalizedId),
580
621
  activeOperator: existing.activeOperator?.id === normalizedId ? null : existing.activeOperator
581
622
  });
582
- await appendRoundtableEvent(
583
- cwd,
584
- `- ${new Date().toISOString()}: member removed ${current.name || current.id}`
585
- );
586
- return next;
623
+ return (await recordSharedProjectEvent(cwd, next, `member removed ${current.name || current.id}`, {
624
+ type: "member-removed",
625
+ actorId: String(options.actorId || "").trim(),
626
+ meta: { memberId: current.id }
627
+ })).project;
587
628
  }
588
629
 
589
630
  export async function listSharedTasks(cwd) {
@@ -618,8 +659,13 @@ export async function addSharedTask(cwd, text = "", options = {}) {
618
659
  ...existing,
619
660
  tasks: [...(existing.tasks || []), task]
620
661
  });
621
- await appendRoundtableEvent(cwd, `- ${new Date().toISOString()}: task added [${task.id}] ${task.text}`);
622
- return { project: next, task };
662
+ const recorded = await recordSharedProjectEvent(cwd, next, `task added [${task.id}] ${task.text}`, {
663
+ type: "task-added",
664
+ actorId: String(options.actorId || "").trim(),
665
+ actorName: String(options.actorName || "").trim(),
666
+ meta: { taskId: task.id }
667
+ });
668
+ return { project: recorded.project, task };
623
669
  }
624
670
 
625
671
  export async function moveSharedTask(cwd, taskId = "", state = "open", options = {}) {
@@ -648,8 +694,13 @@ export async function moveSharedTask(cwd, taskId = "", state = "open", options =
648
694
  ...existing,
649
695
  tasks
650
696
  });
651
- await appendRoundtableEvent(cwd, `- ${new Date().toISOString()}: task moved [${tasks[index].id}] -> ${normalizedState}`);
652
- return { project: next, task: tasks[index] };
697
+ const recorded = await recordSharedProjectEvent(cwd, next, `task moved [${tasks[index].id}] -> ${normalizedState}`, {
698
+ type: "task-moved",
699
+ actorId: String(options.actorId || "").trim(),
700
+ actorName: String(options.actorName || "").trim(),
701
+ meta: { taskId: tasks[index].id, state: normalizedState }
702
+ });
703
+ return { project: recorded.project, task: tasks[index] };
653
704
  }
654
705
 
655
706
  export async function assignSharedTask(cwd, taskId = "", memberId = "", options = {}) {
@@ -678,8 +729,13 @@ export async function assignSharedTask(cwd, taskId = "", memberId = "", options
678
729
  ...existing,
679
730
  tasks
680
731
  });
681
- await appendRoundtableEvent(cwd, `- ${new Date().toISOString()}: task assigned [${tasks[index].id}] -> ${normalizedMemberId}`);
682
- return { project: next, task: tasks[index] };
732
+ const recorded = await recordSharedProjectEvent(cwd, next, `task assigned [${tasks[index].id}] -> ${normalizedMemberId}`, {
733
+ type: "task-assigned",
734
+ actorId: String(options.actorId || "").trim(),
735
+ actorName: String(options.actorName || "").trim(),
736
+ meta: { taskId: tasks[index].id, memberId: normalizedMemberId }
737
+ });
738
+ return { project: recorded.project, task: tasks[index] };
683
739
  }
684
740
 
685
741
  export async function claimSharedTask(cwd, taskId = "", options = {}) {
@@ -705,8 +761,13 @@ export async function claimSharedTask(cwd, taskId = "", options = {}) {
705
761
  ...existing,
706
762
  tasks
707
763
  });
708
- await appendRoundtableEvent(cwd, `- ${new Date().toISOString()}: task claimed [${tasks[index].id}] by ${actorId}`);
709
- return { project: next, task: tasks[index] };
764
+ const recorded = await recordSharedProjectEvent(cwd, next, `task claimed [${tasks[index].id}] by ${actorId}`, {
765
+ type: "task-claimed",
766
+ actorId,
767
+ actorName: String(options.actorName || "").trim(),
768
+ meta: { taskId: tasks[index].id }
769
+ });
770
+ return { project: recorded.project, task: tasks[index] };
710
771
  }
711
772
 
712
773
  export async function commentSharedTask(cwd, taskId = "", text = "", options = {}) {
@@ -735,8 +796,13 @@ export async function commentSharedTask(cwd, taskId = "", text = "", options = {
735
796
  ...existing,
736
797
  tasks
737
798
  });
738
- await appendRoundtableEvent(cwd, `- ${new Date().toISOString()}: task comment [${tasks[index].id}] ${normalizedText}`);
739
- return { project: next, task: tasks[index], comment };
799
+ const recorded = await recordSharedProjectEvent(cwd, next, `task comment [${tasks[index].id}] ${normalizedText}`, {
800
+ type: "task-comment",
801
+ actorId: String(options.actorId || "").trim(),
802
+ actorName: String(options.actorName || "").trim(),
803
+ meta: { taskId: tasks[index].id }
804
+ });
805
+ return { project: recorded.project, task: tasks[index], comment };
740
806
  }
741
807
 
742
808
  export async function getSharedTaskHistory(cwd, taskId = "", options = {}) {
@@ -778,8 +844,11 @@ export async function claimSharedOperator(cwd, operator = {}) {
778
844
  members,
779
845
  activeOperator: nextOperator
780
846
  });
781
- await appendRoundtableEvent(cwd, `- ${new Date().toISOString()}: operator claimed by ${nextOperator.name || nextOperator.id}`);
782
- return next;
847
+ return (await recordSharedProjectEvent(cwd, next, `operator claimed by ${nextOperator.name || nextOperator.id}`, {
848
+ type: "operator-claimed",
849
+ actorId: nextOperator.id,
850
+ actorName: nextOperator.name
851
+ })).project;
783
852
  }
784
853
 
785
854
  export async function releaseSharedOperator(cwd, operatorId = "", options = {}) {
@@ -799,8 +868,10 @@ export async function releaseSharedOperator(cwd, operatorId = "", options = {})
799
868
  ...existing,
800
869
  activeOperator: null
801
870
  });
802
- await appendRoundtableEvent(cwd, `- ${new Date().toISOString()}: operator released`);
803
- return next;
871
+ return (await recordSharedProjectEvent(cwd, next, "operator released", {
872
+ type: "operator-released",
873
+ actorId
874
+ })).project;
804
875
  }
805
876
 
806
877
  export function formatSharedProjectStatus(project) {
@@ -818,7 +889,8 @@ export function formatSharedProjectStatus(project) {
818
889
  activeOperator: project.activeOperator,
819
890
  members: project.members,
820
891
  pendingInvites: project.pendingInvites,
821
- tasks: project.tasks
892
+ tasks: project.tasks,
893
+ recentEvents: project.recentEvents
822
894
  }, null, 2);
823
895
  }
824
896