@tritard/waterbrother 0.16.41 → 0.16.43

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 invites`, `waterbrother room invite`, `waterbrother room invite accept`, and `waterbrother room remove`
269
+ - inspect recent collaboration activity with `waterbrother room events`
269
270
  - manage the shared backlog with `waterbrother room tasks`, `waterbrother room task add`, and `waterbrother room task move`
270
271
  - assign, claim, and discuss shared work with `waterbrother room task assign`, `waterbrother room task claim`, `waterbrother room task comment`, and `waterbrother room task history`
271
272
  - choose a shared execution preset with `waterbrother room runtime <profile>`
@@ -295,7 +296,8 @@ Current Telegram behavior:
295
296
  - pending pairings are explicit and expire automatically after 12 hours unless approved
296
297
  - paired Telegram users drive the same live session and permissions as the terminal operator when the TUI bridge is attached
297
298
  - Telegram now supports remote workspace control with `/cwd`, `/use <path>`, `/desktop`, and `/new-project <name>`
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
+ - shared projects now support `/room`, `/events`, `/members`, `/invites`, `/whoami`, `/people`, `/tasks`, `/mode`, `/claim`, `/release`, `/invite`, `/accept-invite`, `/approve-invite`, `/reject-invite`, `/remove-member`, `/room-runtime`, and `/task ...` from Telegram
300
+ - Telegram now supports `/whoami` for Telegram-id and shared-room membership visibility, plus `/events` for recent shared-room activity
299
301
  - `/room` now includes pending invite count plus task ownership summaries
300
302
  - local `/status` now includes `sharedRoom` with pending invites, task ownership summary, and recent shared-room event activity
301
303
  - the TUI now prints a small Roundtable event feed when new shared-room activity lands
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.41",
3
+ "version": "0.16.43",
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
@@ -81,6 +81,7 @@ import {
81
81
  formatSharedProjectStatus,
82
82
  getSharedTaskHistory,
83
83
  getSharedProjectPaths,
84
+ listSharedEvents,
84
85
  listSharedInvites,
85
86
  listSharedMembers,
86
87
  listSharedTasks,
@@ -178,6 +179,7 @@ const INTERACTIVE_COMMANDS = [
178
179
  { name: "/share-project", description: "Enable shared-project mode in the current cwd" },
179
180
  { name: "/unshare-project", description: "Disable shared-project mode in the current cwd" },
180
181
  { name: "/room", description: "Show shared room status for the current project" },
182
+ { name: "/room events", description: "Show recent Roundtable room events" },
181
183
  { name: "/room members", description: "List shared-project members" },
182
184
  { name: "/room invites", description: "List pending shared-project invites" },
183
185
  { name: "/room add <id> [owner|editor|observer]", description: "Add or update a shared-project member" },
@@ -4047,6 +4049,22 @@ async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false,
4047
4049
  return;
4048
4050
  }
4049
4051
 
4052
+ if (sub === "events") {
4053
+ const events = await listSharedEvents(cwd, 12);
4054
+ if (asJson) {
4055
+ printData({ ok: true, events }, true);
4056
+ return;
4057
+ }
4058
+ if (!events.length) {
4059
+ console.log("No recent shared-room events");
4060
+ return;
4061
+ }
4062
+ for (const event of events) {
4063
+ console.log(`${event.createdAt}\t${event.type}\t${event.actorName || event.actorId || "-"}\t${event.text}`);
4064
+ }
4065
+ return;
4066
+ }
4067
+
4050
4068
  if (sub === "tasks") {
4051
4069
  const tasks = await listSharedTasks(cwd);
4052
4070
  if (asJson) {
@@ -4296,7 +4314,7 @@ async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false,
4296
4314
  return;
4297
4315
  }
4298
4316
 
4299
- throw new Error("Usage: waterbrother room status|members|invites|add <member-id> [owner|editor|observer] [display name]|invite <member-id> [owner|editor|observer] [display name]|invite approve <invite-id>|invite accept <invite-id>|invite reject <invite-id>|remove <member-id>|tasks|task add <text>|task assign <id> <member-id>|task claim <id>|task move <id> <open|active|blocked|done>|task comment <id> <text>|task history <id>|runtime [<name>|clear]|mode <chat|plan|execute>|claim|release");
4317
+ throw new Error("Usage: waterbrother room status|events|members|invites|add <member-id> [owner|editor|observer] [display name]|invite <member-id> [owner|editor|observer] [display name]|invite approve <invite-id>|invite accept <invite-id>|invite reject <invite-id>|remove <member-id>|tasks|task add <text>|task assign <id> <member-id>|task claim <id>|task move <id> <open|active|blocked|done>|task comment <id> <text>|task history <id>|runtime [<name>|clear]|mode <chat|plan|execute>|claim|release");
4300
4318
  }
4301
4319
 
4302
4320
  async function runMcpCommand(positional, runtime, { cwd, asJson = false } = {}) {
@@ -8241,6 +8259,15 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
8241
8259
  continue;
8242
8260
  }
8243
8261
 
8262
+ if (line === "/room events") {
8263
+ try {
8264
+ await runRoomCommand(["room", "events"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
8265
+ } catch (error) {
8266
+ console.log(`room events failed: ${error instanceof Error ? error.message : String(error)}`);
8267
+ }
8268
+ continue;
8269
+ }
8270
+
8244
8271
  if (line === "/room tasks") {
8245
8272
  try {
8246
8273
  await runRoomCommand(["room", "tasks"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
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 { acceptSharedInvite, addSharedTask, approveSharedInvite, assignSharedTask, claimSharedOperator, claimSharedTask, commentSharedTask, createSharedInvite, getSharedTaskHistory, listSharedInvites, listSharedTasks, loadSharedProject, moveSharedTask, rejectSharedInvite, releaseSharedOperator, setSharedRoom, setSharedRoomMode, setSharedRuntimeProfile, removeSharedMember } from "./shared-project.js";
11
+ import { acceptSharedInvite, addSharedTask, approveSharedInvite, assignSharedTask, claimSharedOperator, claimSharedTask, commentSharedTask, createSharedInvite, getSharedTaskHistory, listSharedEvents, listSharedInvites, listSharedTasks, loadSharedProject, moveSharedTask, rejectSharedInvite, releaseSharedOperator, setSharedRoom, setSharedRoomMode, setSharedRuntimeProfile, removeSharedMember } from "./shared-project.js";
12
12
  import { buildSelfAwarenessManifest, formatAboutWaterbrother, formatSelfState, resolveLocalConceptQuestion } from "./self-awareness.js";
13
13
 
14
14
  const execFileAsync = promisify(execFile);
@@ -27,9 +27,12 @@ const TELEGRAM_COMMANDS = [
27
27
  { command: "state", description: "Show current Waterbrother self-awareness state" },
28
28
  { command: "run", description: "Execute an explicit remote prompt" },
29
29
  { command: "status", description: "Show the linked remote session" },
30
+ { command: "whoami", description: "Show your Telegram identity and room membership" },
31
+ { command: "people", description: "Show recently seen Telegram people in this chat" },
30
32
  { command: "cwd", description: "Show the current remote working directory" },
31
33
  { command: "runtime", description: "Show active runtime status" },
32
34
  { command: "room", description: "Show shared room status" },
35
+ { command: "events", description: "Show recent shared room events" },
33
36
  { command: "members", description: "List shared room members" },
34
37
  { command: "invites", description: "List pending shared room invites" },
35
38
  { command: "tasks", description: "List shared Roundtable tasks" },
@@ -204,12 +207,15 @@ function buildRemoteHelp() {
204
207
  "<code>/state</code> show current Waterbrother self-awareness state",
205
208
  "<code>/run &lt;prompt&gt;</code> execute an explicit remote request",
206
209
  "<code>/status</code> show the current linked remote session",
210
+ "<code>/whoami</code> show your Telegram identity and room membership",
211
+ "<code>/people</code> list recent Telegram users in this chat for easier invites",
207
212
  "<code>/cwd</code> show the current remote working directory",
208
213
  "<code>/use &lt;path&gt;</code> switch the linked session to another directory",
209
214
  "<code>/desktop</code> switch the linked session to <code>~/Desktop</code>",
210
215
  "<code>/new-project &lt;name&gt;</code> create a folder on Desktop and switch into it",
211
216
  "<code>/runtime</code> show active provider/model/runtime state",
212
217
  "<code>/room</code> show shared project room status",
218
+ "<code>/events</code> show recent shared room events",
213
219
  "<code>/members</code> list shared project members",
214
220
  "<code>/tasks</code> list shared project tasks",
215
221
  "<code>/task add &lt;text&gt;</code> add a shared Roundtable task",
@@ -220,7 +226,8 @@ function buildRemoteHelp() {
220
226
  "<code>/task history &lt;id&gt;</code> show task history and comments",
221
227
  "<code>/room-runtime</code> or <code>/room-runtime &lt;name|clear&gt;</code> inspect or set the shared room runtime profile",
222
228
  "<code>/invites</code> list pending shared invites",
223
- "<code>/invite &lt;user-id&gt; [owner|editor|observer]</code> create a pending shared-project invite",
229
+ "<code>/invite &lt;user-id|@username&gt; [owner|editor|observer]</code> create a pending shared-project invite",
230
+ "reply to a teammate and use <code>/invite [owner|editor|observer]</code> to invite that person directly",
224
231
  "<code>/accept-invite &lt;invite-id&gt;</code> accept your own pending invite",
225
232
  "<code>/approve-invite &lt;invite-id&gt;</code> approve a pending invite",
226
233
  "<code>/reject-invite &lt;invite-id&gt;</code> reject a pending invite",
@@ -388,6 +395,79 @@ function formatTelegramInvitesMarkup(invites = []) {
388
395
  ].join("\n");
389
396
  }
390
397
 
398
+ function formatTelegramEventsMarkup(events = []) {
399
+ if (!events.length) {
400
+ return "<b>Recent room events</b>\n• none";
401
+ }
402
+ return [
403
+ "<b>Recent room events</b>",
404
+ ...events.map((event) => {
405
+ const actor = String(event.actorName || event.actorId || "").trim();
406
+ const when = String(event.createdAt || "").trim();
407
+ return `• ${actor ? `${escapeTelegramHtml(actor)} — ` : ""}${escapeTelegramHtml(event.text || "")}${when ? `\n <code>${escapeTelegramHtml(when)}</code>` : ""}`;
408
+ })
409
+ ].join("\n");
410
+ }
411
+
412
+ function formatTelegramWhoamiMarkup({ message, member = null, invites = [], sharedEnabled = false }) {
413
+ const from = message?.from || {};
414
+ const userId = String(from.id || "").trim();
415
+ const username = String(from.username || "").trim();
416
+ const displayName = [from.first_name, from.last_name].filter(Boolean).join(" ").trim() || username || userId;
417
+ const pending = invites.filter((invite) => String(invite.memberId || "").trim() === userId);
418
+ const lines = [
419
+ "<b>Telegram identity</b>",
420
+ `name: <code>${escapeTelegramHtml(displayName)}</code>`,
421
+ `user id: <code>${escapeTelegramHtml(userId)}</code>`,
422
+ `username: <code>${escapeTelegramHtml(username || "none")}</code>`
423
+ ];
424
+ if (!sharedEnabled) {
425
+ lines.push("shared room: <code>off</code>");
426
+ lines.push("This project is not shared yet. Start with <code>waterbrother project share</code>.");
427
+ return lines.join("\n");
428
+ }
429
+ lines.push(`shared member: <code>${member ? "yes" : "no"}</code>`);
430
+ if (member) {
431
+ lines.push(`role: <code>${escapeTelegramHtml(member.role || "editor")}</code>`);
432
+ }
433
+ if (pending.length) {
434
+ lines.push("<b>Pending invites you can accept</b>");
435
+ lines.push(...pending.map((invite) => `• <code>${escapeTelegramHtml(invite.id)}</code> <i>(${escapeTelegramHtml(invite.role || "editor")})</i>`));
436
+ lines.push("Use <code>/accept-invite &lt;invite-id&gt;</code> to join the shared room.");
437
+ } else {
438
+ lines.push("No pending Waterbrother room invite for this Telegram user id.");
439
+ }
440
+ lines.push("Use <code>/people</code> in a shared chat to see recent Telegram ids and usernames you can invite.");
441
+ return lines.join("\n");
442
+ }
443
+
444
+ function formatTelegramPeopleMarkup({ people = [], project = null }) {
445
+ if (!people.length) {
446
+ return [
447
+ "<b>Recent Telegram people</b>",
448
+ "• none",
449
+ "Talk in the shared chat first, then run <code>/people</code> again."
450
+ ].join("\n");
451
+ }
452
+ const members = Array.isArray(project?.members) ? project.members : [];
453
+ const pendingInvites = Array.isArray(project?.pendingInvites) ? project.pendingInvites : [];
454
+ return [
455
+ "<b>Recent Telegram people</b>",
456
+ ...people.map((person) => {
457
+ const member = members.find((entry) => String(entry?.id || "").trim() === person.userId);
458
+ const pendingInvite = pendingInvites.find((entry) => String(entry?.memberId || "").trim() === person.userId);
459
+ const bits = [`<code>${escapeTelegramHtml(person.userId)}</code>`];
460
+ if (person.usernameHandle) bits.push(`@${escapeTelegramHtml(person.usernameHandle)}`);
461
+ bits.push(escapeTelegramHtml(person.displayName || person.userId));
462
+ if (member?.role) bits.push(`<i>member:${escapeTelegramHtml(member.role)}</i>`);
463
+ else if (pendingInvite?.id) bits.push(`<i>pending:${escapeTelegramHtml(pendingInvite.id)}</i>`);
464
+ else if (person.paired) bits.push("<i>paired</i>");
465
+ return `• ${bits.join(" ")}`;
466
+ }),
467
+ "Invite by id, by @username, or by replying to a message with <code>/invite [role]</code>."
468
+ ].join("\n");
469
+ }
470
+
391
471
  function formatTelegramTasksMarkup(tasks = []) {
392
472
  if (!tasks.length) {
393
473
  return "<b>Shared tasks</b>\n• none";
@@ -484,12 +564,15 @@ function suggestTaskText(text = "") {
484
564
 
485
565
  function parseInviteCommand(text) {
486
566
  const parts = String(text || "").trim().split(/\s+/).filter(Boolean);
487
- const userId = String(parts[1] || "").trim();
567
+ let target = String(parts[1] || "").trim();
488
568
  let role = "editor";
489
- if (parts[2] && ["owner", "editor", "observer"].includes(String(parts[2]).toLowerCase())) {
569
+ if (target && ["owner", "editor", "observer"].includes(target.toLowerCase())) {
570
+ role = target.toLowerCase();
571
+ target = "";
572
+ } else if (parts[2] && ["owner", "editor", "observer"].includes(String(parts[2]).toLowerCase())) {
490
573
  role = String(parts[2]).toLowerCase();
491
574
  }
492
- return { userId, role };
575
+ return { target, role };
493
576
  }
494
577
 
495
578
  function extractRetryDelayMs(error, attempt) {
@@ -674,12 +757,100 @@ class TelegramGateway {
674
757
  await saveGatewayState("telegram", this.state);
675
758
  }
676
759
 
760
+ describeTelegramUser(from = {}) {
761
+ const userId = String(from?.id || "").trim();
762
+ const usernameHandle = String(from?.username || "").trim();
763
+ const displayName = [from?.first_name, from?.last_name].filter(Boolean).join(" ").trim() || usernameHandle || userId;
764
+ return { userId, usernameHandle, displayName };
765
+ }
766
+
767
+ listKnownChatPeople(message) {
768
+ const chatId = String(message?.chat?.id || "").trim();
769
+ const byId = new Map();
770
+ const upsert = (person = {}) => {
771
+ const userId = String(person.userId || person.id || "").trim();
772
+ if (!userId) return;
773
+ const existing = byId.get(userId) || { userId, usernameHandle: "", displayName: userId, paired: false };
774
+ byId.set(userId, {
775
+ ...existing,
776
+ ...person,
777
+ userId,
778
+ usernameHandle: String(person.usernameHandle || existing.usernameHandle || "").trim(),
779
+ displayName: String(person.displayName || existing.displayName || userId).trim() || userId,
780
+ paired: person.paired === true || existing.paired === true
781
+ });
782
+ };
783
+
784
+ for (const [userId, peer] of Object.entries(this.state.peers || {})) {
785
+ if (String(peer?.chatId || "").trim() !== chatId) continue;
786
+ upsert({
787
+ userId,
788
+ usernameHandle: String(peer?.usernameHandle || "").trim(),
789
+ displayName: String(peer?.displayName || peer?.username || userId).trim(),
790
+ paired: true,
791
+ lastSeenAt: String(peer?.lastSeenAt || "").trim()
792
+ });
793
+ }
794
+
795
+ for (const [userId, pending] of Object.entries(this.state.pendingPairings || {})) {
796
+ if (String(pending?.chatId || "").trim() !== chatId) continue;
797
+ upsert({
798
+ userId,
799
+ usernameHandle: String(pending?.usernameHandle || "").trim(),
800
+ displayName: String(pending?.displayName || pending?.username || userId).trim(),
801
+ paired: false,
802
+ lastSeenAt: String(pending?.lastSeenAt || pending?.firstSeenAt || "").trim()
803
+ });
804
+ }
805
+
806
+ const current = this.describeTelegramUser(message?.from || {});
807
+ if (current.userId) upsert({ ...current, paired: this.isAuthorizedUser(message?.from) === true });
808
+ const replied = this.describeTelegramUser(message?.reply_to_message?.from || {});
809
+ if (replied.userId) upsert({ ...replied, paired: this.isAuthorizedUser(message?.reply_to_message?.from) === true });
810
+
811
+ return [...byId.values()].sort((a, b) => {
812
+ const aSeen = Date.parse(a.lastSeenAt || "") || 0;
813
+ const bSeen = Date.parse(b.lastSeenAt || "") || 0;
814
+ return bSeen - aSeen || a.displayName.localeCompare(b.displayName);
815
+ });
816
+ }
817
+
818
+ resolveInviteTarget(message, target) {
819
+ const value = String(target || "").trim();
820
+ const known = this.listKnownChatPeople(message);
821
+ const replied = this.describeTelegramUser(message?.reply_to_message?.from || {});
822
+
823
+ if (!value) {
824
+ if (replied.userId) {
825
+ const match = known.find((person) => person.userId === replied.userId) || replied;
826
+ return { userId: replied.userId, displayName: match.displayName || replied.displayName };
827
+ }
828
+ throw new Error("Usage: /invite <user-id|@username> [owner|editor|observer], or reply to someone with /invite [role]");
829
+ }
830
+
831
+ if (/^\d+$/.test(value)) {
832
+ const match = known.find((person) => person.userId === value);
833
+ return { userId: value, displayName: match?.displayName || value };
834
+ }
835
+
836
+ const normalized = value.replace(/^@/, "").trim().toLowerCase();
837
+ const byUsername = known.filter((person) => String(person.usernameHandle || "").trim().toLowerCase() === normalized);
838
+ if (byUsername.length === 1) {
839
+ return { userId: byUsername[0].userId, displayName: byUsername[0].displayName || byUsername[0].userId };
840
+ }
841
+ if (byUsername.length > 1) {
842
+ throw new Error(`Multiple Telegram users matched @${normalized}. Use /people and invite by numeric id.`);
843
+ }
844
+ throw new Error(`Unknown Telegram user reference: ${value}. Use /people to list recent users in this chat.`);
845
+ }
846
+
677
847
  async addPendingPairing(message) {
678
- const userId = String(message?.from?.id || "").trim();
679
- const username = [message?.from?.username, message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim();
848
+ const { userId, usernameHandle, displayName } = this.describeTelegramUser(message?.from || {});
680
849
  this.state.pendingPairings[userId] = {
681
850
  userId,
682
- username,
851
+ username: displayName,
852
+ usernameHandle,
853
+ displayName,
683
854
  chatId: String(message.chat.id),
684
855
  firstSeenAt: this.state.pendingPairings[userId]?.firstSeenAt || new Date().toISOString(),
685
856
  lastSeenAt: new Date().toISOString(),
@@ -714,14 +885,15 @@ class TelegramGateway {
714
885
  }
715
886
 
716
887
  async ensurePeerSession(message) {
717
- const userId = String(message?.from?.id || "").trim();
718
- const username = [message?.from?.username, message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim();
888
+ const { userId, usernameHandle, displayName } = this.describeTelegramUser(message?.from || {});
719
889
  const existing = this.getPeerState(userId);
720
890
  if (existing?.sessionId) {
721
891
  this.state.peers[userId] = {
722
892
  ...existing,
723
893
  chatId: String(message.chat.id),
724
- username,
894
+ username: displayName,
895
+ usernameHandle,
896
+ displayName,
725
897
  sessions: upsertSessionHistory(existing.sessions, existing.sessionId),
726
898
  lastSeenAt: new Date().toISOString(),
727
899
  lastMessageId: message.message_id
@@ -744,7 +916,9 @@ class TelegramGateway {
744
916
  this.state.peers[userId] = {
745
917
  sessionId: session.id,
746
918
  chatId: String(message.chat.id),
747
- username,
919
+ username: displayName,
920
+ usernameHandle,
921
+ displayName,
748
922
  sessions: upsertSessionHistory([], session.id),
749
923
  linkedAt: new Date().toISOString(),
750
924
  lastSeenAt: new Date().toISOString(),
@@ -773,7 +947,9 @@ class TelegramGateway {
773
947
  ...previous,
774
948
  sessionId: session.id,
775
949
  chatId: String(message.chat.id),
776
- username: [message?.from?.username, message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim(),
950
+ username: this.describeTelegramUser(message?.from || {}).displayName,
951
+ usernameHandle: this.describeTelegramUser(message?.from || {}).usernameHandle,
952
+ displayName: this.describeTelegramUser(message?.from || {}).displayName,
777
953
  sessions: upsertSessionHistory(previous.sessions, session.id),
778
954
  linkedAt: new Date().toISOString(),
779
955
  lastSeenAt: new Date().toISOString(),
@@ -804,7 +980,9 @@ class TelegramGateway {
804
980
  ...previous,
805
981
  sessionId: session.id,
806
982
  chatId: String(message.chat.id),
807
- username: [message?.from?.username, message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim(),
983
+ username: this.describeTelegramUser(message?.from || {}).displayName,
984
+ usernameHandle: this.describeTelegramUser(message?.from || {}).usernameHandle,
985
+ displayName: this.describeTelegramUser(message?.from || {}).displayName,
808
986
  sessions: upsertSessionHistory(previous.sessions, session.id),
809
987
  linkedAt: previous.linkedAt || new Date().toISOString(),
810
988
  lastSeenAt: new Date().toISOString(),
@@ -928,7 +1106,9 @@ class TelegramGateway {
928
1106
  id: requestId,
929
1107
  chatId: String(message.chat.id),
930
1108
  userId: String(message?.from?.id || ""),
931
- username: [message?.from?.username, message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim(),
1109
+ username: this.describeTelegramUser(message?.from || {}).displayName,
1110
+ usernameHandle: this.describeTelegramUser(message?.from || {}).usernameHandle,
1111
+ displayName: this.describeTelegramUser(message?.from || {}).displayName,
932
1112
  sessionId: String(sessionId || "").trim(),
933
1113
  text: String(promptText || "").trim(),
934
1114
  runtimeProfile: String(project?.runtimeProfile || "").trim(),
@@ -1152,6 +1332,30 @@ class TelegramGateway {
1152
1332
  return;
1153
1333
  }
1154
1334
 
1335
+ if (text === "/whoami") {
1336
+ const { project } = await this.bindSharedRoomForMessage(message, sessionId);
1337
+ const invites = project?.enabled ? await listSharedInvites(sessionCwd).catch(() => []) : [];
1338
+ const member = Array.isArray(project?.members)
1339
+ ? project.members.find((entry) => String(entry?.id || "").trim() === userId) || null
1340
+ : null;
1341
+ await this.sendMessage(
1342
+ message.chat.id,
1343
+ formatTelegramWhoamiMarkup({ message, member, invites, sharedEnabled: project?.enabled === true }),
1344
+ message.message_id
1345
+ );
1346
+ return;
1347
+ }
1348
+
1349
+ if (text === "/people") {
1350
+ const { project } = await this.bindSharedRoomForMessage(message, sessionId);
1351
+ await this.sendMessage(
1352
+ message.chat.id,
1353
+ formatTelegramPeopleMarkup({ people: this.listKnownChatPeople(message), project }),
1354
+ message.message_id
1355
+ );
1356
+ return;
1357
+ }
1358
+
1155
1359
  if (text === "/status") {
1156
1360
  await this.sendMessage(
1157
1361
  message.chat.id,
@@ -1195,6 +1399,21 @@ class TelegramGateway {
1195
1399
  return;
1196
1400
  }
1197
1401
 
1402
+ if (text === "/events") {
1403
+ const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
1404
+ if (!project?.enabled) {
1405
+ await this.sendMessage(message.chat.id, "This project is not shared.", message.message_id);
1406
+ return;
1407
+ }
1408
+ try {
1409
+ const events = await listSharedEvents(session.cwd || this.cwd, 12);
1410
+ await this.sendMessage(message.chat.id, formatTelegramEventsMarkup(events), message.message_id);
1411
+ } catch (error) {
1412
+ await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
1413
+ }
1414
+ return;
1415
+ }
1416
+
1198
1417
  if (text === "/tasks") {
1199
1418
  const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
1200
1419
  if (!project?.enabled) {
@@ -1316,20 +1535,18 @@ class TelegramGateway {
1316
1535
  await this.sendMessage(message.chat.id, "This project is not shared.", message.message_id);
1317
1536
  return;
1318
1537
  }
1319
- const { userId: nextUserId, role } = parseInviteCommand(text);
1320
- if (!nextUserId) {
1321
- await this.sendMessage(message.chat.id, "Usage: /invite <user-id> [owner|editor|observer]", message.message_id);
1322
- return;
1323
- }
1538
+ const { target, role } = parseInviteCommand(text);
1324
1539
  try {
1540
+ const inviteTarget = this.resolveInviteTarget(message, target);
1325
1541
  const result = await createSharedInvite(
1326
1542
  session.cwd || this.cwd,
1327
- { id: nextUserId, role, name: nextUserId, paired: true },
1543
+ { id: inviteTarget.userId, role, name: inviteTarget.displayName || inviteTarget.userId, paired: true },
1328
1544
  { actorId: userId, actorName: peer?.username || [message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim() || userId }
1329
1545
  );
1330
1546
  await this.sendMessage(
1331
1547
  message.chat.id,
1332
- `Created pending invite <code>${escapeTelegramHtml(result.invite.id)}</code> for <code>${escapeTelegramHtml(result.invite.memberId)}</code> <i>(${escapeTelegramHtml(result.invite.role)})</i>`,
1548
+ `Created pending invite <code>${escapeTelegramHtml(result.invite.id)}</code> for <code>${escapeTelegramHtml(result.invite.memberId)}</code> <i>(${escapeTelegramHtml(result.invite.role)})</i>
1549
+ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTelegramHtml(result.invite.id)}</code>.`,
1333
1550
  message.message_id
1334
1551
  );
1335
1552
  } catch (error) {
@@ -98,6 +98,223 @@ function buildProjectMapSummary({ entries = [], packageJson = null, sharedProjec
98
98
  };
99
99
  }
100
100
 
101
+ function collectSources(...groups) {
102
+ return [...new Set(groups.flat().filter(Boolean))];
103
+ }
104
+
105
+ function commandEntry(id, summary, commands = [], options = {}) {
106
+ return {
107
+ id,
108
+ summary,
109
+ commands,
110
+ surfaces: options.surfaces || ["shell", "tui", "telegram"],
111
+ keywords: options.keywords || [id],
112
+ sources: options.sources || []
113
+ };
114
+ }
115
+
116
+ function capabilityEntry(id, summary, options = {}) {
117
+ return {
118
+ id,
119
+ summary,
120
+ keywords: options.keywords || [id],
121
+ sources: options.sources || []
122
+ };
123
+ }
124
+
125
+ function workflowEntry(id, summary, steps = [], options = {}) {
126
+ return {
127
+ id,
128
+ summary,
129
+ steps,
130
+ keywords: options.keywords || [id],
131
+ sources: options.sources || []
132
+ };
133
+ }
134
+
135
+ function buildCommandRegistry({ sourceHints = {}, sharedRoom = {} } = {}) {
136
+ return [
137
+ commandEntry("share-project", "Enable shared-project mode in the current repo and create Roundtable state files.", [
138
+ "waterbrother project share",
139
+ "/share-project"
140
+ ], { keywords: ["share project", "shared project", "enable roundtable", "enable shared room"], sources: [sourceHints.roundtableDocs, sourceHints.sharedState] }),
141
+ commandEntry("room-status", "Inspect the shared room, recent events, members, invites, tasks, and runtime.", [
142
+ "waterbrother room status",
143
+ "waterbrother room events",
144
+ "/room",
145
+ "/events"
146
+ ], { keywords: ["roundtable", "shared room", "room status", "events"], sources: [sourceHints.roundtableDocs, sourceHints.sharedState] }),
147
+ commandEntry("room-membership", "Manage shared-room invites and membership.", [
148
+ "waterbrother room invite <member-id> [owner|editor|observer]",
149
+ "waterbrother room invite accept <invite-id>",
150
+ "/invite <user-id|@username> [owner|editor|observer]",
151
+ "/accept-invite <invite-id>",
152
+ "/whoami",
153
+ "/people"
154
+ ], { keywords: ["invite", "partner", "teammate", "collaborator", "member", "accept invite"], sources: [sourceHints.roundtableDocs, sourceHints.telegramDocs, sourceHints.sharedState] }),
155
+ commandEntry("room-mode", "Set whether a shared room is chatting, planning, or executing.", [
156
+ "waterbrother room mode <chat|plan|execute>",
157
+ "/mode <chat|plan|execute>",
158
+ "/run <prompt>"
159
+ ], { keywords: ["room mode", "execute mode", "plan mode", "chat mode", "group execution"], sources: [sourceHints.roundtableDocs, sourceHints.telegramDocs] }),
160
+ commandEntry("room-operator", "Claim or release the active operator lock for shared execution.", [
161
+ "waterbrother room claim",
162
+ "waterbrother room release",
163
+ "/claim",
164
+ "/release"
165
+ ], { keywords: ["claim", "release", "operator", "lock", "active operator"], sources: [sourceHints.roundtableDocs, sourceHints.sharedState] }),
166
+ commandEntry("room-tasks", "Inspect and manage the shared Roundtable task queue.", [
167
+ "waterbrother room tasks",
168
+ "waterbrother room task add <text>",
169
+ "waterbrother room task assign <id> <member-id>",
170
+ "waterbrother room task move <id> <open|active|blocked|done>",
171
+ "/tasks",
172
+ "/task add <text>",
173
+ "/task assign <id> <member-id>",
174
+ "/task claim <id>",
175
+ "/task move <id> <open|active|blocked|done>"
176
+ ], { keywords: ["task", "tasks", "backlog", "assign task", "roundtable task"], sources: [sourceHints.roundtableDocs, sourceHints.sharedState] }),
177
+ commandEntry("new-project", "Create a new project and switch the live session into it.", [
178
+ "/new-project <name>",
179
+ "/desktop",
180
+ "/use <path>"
181
+ ], { keywords: ["new project", "make project", "create project", "desktop project"], sources: [sourceHints.readme, sourceHints.telegramDocs] }),
182
+ commandEntry("runtime-profiles", "Inspect or select named runtime presets for local and shared execution.", [
183
+ "waterbrother runtime-profiles list",
184
+ "waterbrother room runtime [name|clear]",
185
+ "/runtime-profiles",
186
+ "/room-runtime <name|clear>"
187
+ ], { keywords: ["runtime profile", "runtime profiles", "provider preset", "shared room runtime"], sources: [sourceHints.readme, sourceHints.roundtableDocs] }),
188
+ commandEntry("telegram-gateway", "Inspect and run the Telegram messaging gateway.", [
189
+ "waterbrother gateway status",
190
+ "waterbrother gateway run telegram",
191
+ "waterbrother gateway stop",
192
+ "/gateway"
193
+ ], { keywords: ["telegram", "gateway", "messaging", "telegram adapter"], sources: [sourceHints.telegramDocs] }),
194
+ commandEntry("self-awareness", "Inspect Waterbrother's own repo/runtime understanding.", [
195
+ "waterbrother about",
196
+ "waterbrother capabilities",
197
+ "waterbrother project-map",
198
+ "waterbrother state",
199
+ "/about",
200
+ "/capabilities",
201
+ "/project-map",
202
+ "/state"
203
+ ], { keywords: ["about", "capabilities", "state", "project map", "self awareness", "what are you"], sources: [sourceHints.readme, sourceHints.commandsDocs] })
204
+ ];
205
+ }
206
+
207
+ function buildCapabilityRegistry({ sourceHints = {}, sharedRoom = {} } = {}) {
208
+ const sharedEnabled = sharedRoom.enabled === true;
209
+ return [
210
+ capabilityEntry("interactive-terminal", "Interactive terminal coding with sessions, approvals, tools, receipts, and local repo awareness.", { keywords: ["terminal", "tui", "interactive", "coding cli"], sources: [sourceHints.readme] }),
211
+ capabilityEntry("telegram-bridge", `Telegram messaging gateway with ${sharedEnabled ? "live TUI bridging and shared-room controls" : "pairing, remote sessions, and shared-room support when enabled"}.`, { keywords: ["telegram", "bot", "bridge", "remote control", "messaging"], sources: [sourceHints.telegramDocs] }),
212
+ capabilityEntry("shared-projects", `${sharedEnabled ? "Shared-project collaboration is enabled in this repo." : "Shared-project collaboration is available and currently disabled in this repo."} It uses one room, explicit roles, room modes, an operator lock, Roundtable tasks, and a shared runtime.`, { keywords: ["roundtable", "shared project", "shared room", "collaboration"], sources: [sourceHints.roundtableDocs, sourceHints.sharedState] }),
213
+ capabilityEntry("runtime-profiles", "Bring-your-own-model runtime switching with named runtime profiles and room-level shared runtime selection.", { keywords: ["runtime", "profiles", "provider", "model", "byom"], sources: [sourceHints.readme, sourceHints.roundtableDocs] }),
214
+ capabilityEntry("session-persistence", "Persisted sessions that store cwd, runtime state, and messages for local and Telegram resumes.", { keywords: ["session", "sessions", "resume", "history"], sources: [sourceHints.readme, sourceHints.sharedState] }),
215
+ capabilityEntry("self-awareness", "Local self-awareness generated from repo state, docs, product metadata, shared-room state, and live runtime context.", { keywords: ["self awareness", "know itself", "what are you", "harness", "code base"], sources: [sourceHints.readme, sourceHints.sharedState, sourceHints.commandsDocs] })
216
+ ];
217
+ }
218
+
219
+ function buildWorkflowRegistry({ sourceHints = {} } = {}) {
220
+ return [
221
+ workflowEntry("invite-partner", "Add a partner to a shared Telegram collaboration room.", [
222
+ "Add the human to the Telegram group/chat itself using normal Telegram controls.",
223
+ "Enable shared-project mode with waterbrother project share if the repo is not shared yet.",
224
+ "Use /people or /whoami to discover Telegram ids already seen in the room.",
225
+ "Create a Waterbrother shared-room invite with waterbrother room invite <member-id> [owner|editor|observer] or /invite <user-id|@username> [owner|editor|observer].",
226
+ "Have the invited person accept with waterbrother room invite accept <invite-id> or /accept-invite <invite-id>."
227
+ ], { keywords: ["invite partner", "invite teammate", "add collaborator", "join shared room"], sources: [sourceHints.roundtableDocs, sourceHints.telegramDocs, sourceHints.sharedState] }),
228
+ workflowEntry("make-project", "Create a new project and optionally make it collaborative.", [
229
+ "Use /new-project <name> in the TUI or Telegram to create a Desktop project and switch into it.",
230
+ "If prompted in the TUI, choose whether the new project should be shared.",
231
+ "Use waterbrother project share afterward if you want to enable Roundtable on an existing repo.",
232
+ "Set room mode and claim the operator lock before shared execution."
233
+ ], { keywords: ["make new project", "create project", "new project", "start project"], sources: [sourceHints.readme, sourceHints.roundtableDocs, sourceHints.telegramDocs] }),
234
+ workflowEntry("shared-execution", "Run shared work safely in Telegram groups.", [
235
+ "Use /mode execute for the shared room when the group is ready to act.",
236
+ "Owners or editors claim the operator lock with /claim or waterbrother room claim.",
237
+ "In Telegram groups, explicit execution should use /run <prompt>.",
238
+ "Use /events, /room, and /tasks to keep the group synchronized."
239
+ ], { keywords: ["shared execution", "group execute", "work together in telegram", "run in group"], sources: [sourceHints.roundtableDocs, sourceHints.telegramDocs, sourceHints.sharedState] })
240
+ ];
241
+ }
242
+
243
+ function summarizeRoomState(sharedRoom = {}) {
244
+ if (!sharedRoom.enabled) {
245
+ return "Shared room is off in this repo.";
246
+ }
247
+ const bits = [`Shared room is enabled in ${sharedRoom.roomMode || "chat"} mode.`];
248
+ if (sharedRoom.runtimeProfile) bits.push(`Room runtime profile: ${sharedRoom.runtimeProfile}.`);
249
+ bits.push(`Members: ${sharedRoom.memberCount || 0}, pending invites: ${sharedRoom.pendingInviteCount || 0}, tasks: ${sharedRoom.taskCount || 0}.`);
250
+ if (sharedRoom.activeOperator?.id) bits.push(`Active operator: ${sharedRoom.activeOperator.name || sharedRoom.activeOperator.id}.`);
251
+ return bits.join(" ");
252
+ }
253
+
254
+ function normalizeQuestion(text = "") {
255
+ return String(text || "")
256
+ .toLowerCase()
257
+ .replace(/[^a-z0-9\s/-]+/g, " ")
258
+ .replace(/\s+/g, " ")
259
+ .trim();
260
+ }
261
+
262
+ function scoreEntry(question, entry) {
263
+ const q = normalizeQuestion(question);
264
+ const qTokens = new Set(q.split(" ").filter(Boolean));
265
+ let score = 0;
266
+ for (const keyword of entry.keywords || []) {
267
+ const normalized = normalizeQuestion(keyword);
268
+ if (!normalized) continue;
269
+ if (q.includes(normalized)) score += Math.max(2, normalized.split(" ").length * 2);
270
+ const tokens = normalized.split(" ").filter(Boolean);
271
+ if (tokens.length > 1 && tokens.every((token) => qTokens.has(token))) {
272
+ score += tokens.length + 1;
273
+ }
274
+ }
275
+ if (entry.id && q.includes(normalizeQuestion(entry.id))) score += 2;
276
+ return score;
277
+ }
278
+
279
+ function findBestEntries(question, entries = [], minimumScore = 2) {
280
+ return entries
281
+ .map((entry) => ({ entry, score: scoreEntry(question, entry) }))
282
+ .filter((item) => item.score >= minimumScore)
283
+ .sort((a, b) => b.score - a.score)
284
+ .map((item) => item.entry);
285
+ }
286
+
287
+ function renderWorkflowAnswer(workflow, roomState, capability, commands) {
288
+ const lines = [workflow.summary, ...workflow.steps.map((step, index) => `${index + 1}. ${step}`)];
289
+ if (commands.length) {
290
+ lines.push("Relevant commands:");
291
+ for (const command of commands.slice(0, 2)) {
292
+ lines.push(...command.commands.map((value) => `- ${value}`));
293
+ }
294
+ }
295
+ if (roomState) lines.push(roomState);
296
+ if (capability) lines.push(`Context: ${capability.summary}`);
297
+ const sources = collectSources(workflow.sources, capability?.sources || [], ...commands.map((entry) => entry.sources || []));
298
+ if (sources.length) lines.push(`sources: ${sources.join(", ")}`);
299
+ return lines.join("\n");
300
+ }
301
+
302
+ function renderConceptAnswer({ subject, capability, commands = [], roomState, extra = [] }) {
303
+ const lines = [subject];
304
+ if (capability) lines.push(capability.summary);
305
+ if (roomState) lines.push(roomState);
306
+ if (commands.length) {
307
+ lines.push("Relevant commands:");
308
+ for (const entry of commands.slice(0, 3)) {
309
+ lines.push(...entry.commands.map((value) => `- ${value}`));
310
+ }
311
+ }
312
+ if (extra.length) lines.push(...extra);
313
+ const sources = collectSources(capability?.sources || [], ...commands.map((entry) => entry.sources || []));
314
+ if (sources.length) lines.push(`sources: ${sources.join(", ")}`);
315
+ return lines.join("\n");
316
+ }
317
+
101
318
  export async function buildSelfAwarenessManifest({ cwd, runtime = {}, currentSession = null } = {}) {
102
319
  const [packageJson, readme, memoryText, roundtableText, sharedProject, product, git, entries] = await Promise.all([
103
320
  readJson(path.join(cwd, "package.json")),
@@ -117,6 +334,26 @@ export async function buildSelfAwarenessManifest({ cwd, runtime = {}, currentSes
117
334
  };
118
335
 
119
336
  const projectMap = buildProjectMapSummary({ entries, packageJson, sharedProject, product });
337
+ const sourceHints = {
338
+ readme: await exists(path.join(cwd, "README.md")) ? "README.md" : "",
339
+ projectMemory: await exists(path.join(cwd, "WATERBROTHER.md")) ? "WATERBROTHER.md" : "",
340
+ roundtable: await exists(path.join(cwd, "ROUNDTABLE.md")) ? "ROUNDTABLE.md" : "",
341
+ sharedState: sharedProject ? ".waterbrother/shared.json" : "",
342
+ telegramDocs: docsPresence.telegram ? "channels/telegram/index.html" : "",
343
+ roundtableDocs: docsPresence.roundtable ? "roundtable/index.html" : "",
344
+ commandsDocs: await exists(path.join(cwd, "commands", "index.html")) ? "commands/index.html" : ""
345
+ };
346
+ const sharedRoom = sharedProject
347
+ ? {
348
+ enabled: true,
349
+ roomMode: sharedProject.roomMode || "chat",
350
+ runtimeProfile: sharedProject.runtimeProfile || "",
351
+ memberCount: Array.isArray(sharedProject.members) ? sharedProject.members.length : 0,
352
+ pendingInviteCount: Array.isArray(sharedProject.pendingInvites) ? sharedProject.pendingInvites.length : 0,
353
+ taskCount: Array.isArray(sharedProject.tasks) ? sharedProject.tasks.length : 0,
354
+ activeOperator: sharedProject.activeOperator || null
355
+ }
356
+ : { enabled: false };
120
357
  const manifest = {
121
358
  version: 1,
122
359
  generatedAt: new Date().toISOString(),
@@ -154,17 +391,7 @@ export async function buildSelfAwarenessManifest({ cwd, runtime = {}, currentSes
154
391
  roundtableTitle: firstHeading(roundtableText) || "",
155
392
  projectMap
156
393
  },
157
- sharedRoom: sharedProject
158
- ? {
159
- enabled: true,
160
- roomMode: sharedProject.roomMode || "chat",
161
- runtimeProfile: sharedProject.runtimeProfile || "",
162
- memberCount: Array.isArray(sharedProject.members) ? sharedProject.members.length : 0,
163
- pendingInviteCount: Array.isArray(sharedProject.pendingInvites) ? sharedProject.pendingInvites.length : 0,
164
- taskCount: Array.isArray(sharedProject.tasks) ? sharedProject.tasks.length : 0,
165
- activeOperator: sharedProject.activeOperator || null
166
- }
167
- : { enabled: false },
394
+ sharedRoom,
168
395
  product: product
169
396
  ? {
170
397
  name: product.name || "",
@@ -172,13 +399,18 @@ export async function buildSelfAwarenessManifest({ cwd, runtime = {}, currentSes
172
399
  surfaceCount: Array.isArray(product.surfaces) ? product.surfaces.length : 0
173
400
  }
174
401
  : null,
175
- sourceHints: {
176
- readme: await exists(path.join(cwd, "README.md")) ? "README.md" : "",
177
- projectMemory: await exists(path.join(cwd, "WATERBROTHER.md")) ? "WATERBROTHER.md" : "",
178
- roundtable: await exists(path.join(cwd, "ROUNDTABLE.md")) ? "ROUNDTABLE.md" : "",
179
- sharedState: sharedProject ? ".waterbrother/shared.json" : "",
180
- telegramDocs: docsPresence.telegram ? "channels/telegram/index.html" : "",
181
- roundtableDocs: docsPresence.roundtable ? "roundtable/index.html" : ""
402
+ sourceHints,
403
+ knowledge: {
404
+ harness: {
405
+ surface: "waterbrother",
406
+ runtimeSurface: "local terminal",
407
+ messagingSurfaces: ["telegram"],
408
+ liveBridge: true,
409
+ sharedRoomSummary: summarizeRoomState(sharedRoom)
410
+ },
411
+ commands: buildCommandRegistry({ sourceHints, sharedRoom }),
412
+ capabilities: buildCapabilityRegistry({ sourceHints, sharedRoom }),
413
+ workflows: buildWorkflowRegistry({ sourceHints })
182
414
  }
183
415
  };
184
416
 
@@ -202,7 +434,8 @@ export function buildSelfAwarenessMemoryBlock(manifest = {}) {
202
434
  `- git branch: ${manifest.identity?.git?.branch || "none"}`,
203
435
  `- runtime: ${manifest.runtime?.provider || "unknown"}/${manifest.runtime?.model || "unknown"}`,
204
436
  `- shared room: ${manifest.sharedRoom?.enabled ? `${manifest.sharedRoom.roomMode || "chat"} mode` : "off"}`,
205
- `- capabilities: ${(manifest.features?.capabilities || []).join(", ")}`
437
+ `- capabilities: ${(manifest.features?.capabilities || []).join(", ")}`,
438
+ `- harness: ${manifest.knowledge?.harness?.sharedRoomSummary || "local terminal"}`
206
439
  ];
207
440
  return lines.filter(Boolean).join("\n");
208
441
  }
@@ -210,7 +443,8 @@ export function buildSelfAwarenessMemoryBlock(manifest = {}) {
210
443
  export function formatCapabilitiesSummary(manifest = {}) {
211
444
  return JSON.stringify({
212
445
  identity: manifest.identity,
213
- capabilities: manifest.features?.capabilities || [],
446
+ capabilities: manifest.knowledge?.capabilities || manifest.features?.capabilities || [],
447
+ commands: (manifest.knowledge?.commands || []).map((entry) => ({ id: entry.id, commands: entry.commands })),
214
448
  channels: manifest.features?.channels || [],
215
449
  sharedRoomEnabled: manifest.sharedRoom?.enabled === true
216
450
  }, null, 2);
@@ -239,13 +473,15 @@ export function formatAboutWaterbrother(manifest = {}) {
239
473
  manifest.sourceHints?.readme,
240
474
  manifest.sourceHints?.roundtableDocs,
241
475
  manifest.sourceHints?.telegramDocs,
242
- manifest.sourceHints?.sharedState
476
+ manifest.sourceHints?.sharedState,
477
+ manifest.sourceHints?.commandsDocs
243
478
  ].filter(Boolean);
244
479
  return [
245
480
  `Waterbrother is ${manifest.identity?.description || "a local coding CLI."}`,
246
481
  `repo: ${manifest.identity?.repoName || "-"}`,
247
482
  `cwd: ${manifest.identity?.cwd || "-"}`,
248
483
  `capabilities: ${(manifest.features?.capabilities || []).join(", ") || "-"}`,
484
+ `harness: ${manifest.knowledge?.harness?.sharedRoomSummary || "local terminal"}`,
249
485
  sources.length ? `sources: ${sources.join(", ")}` : ""
250
486
  ].filter(Boolean).join("\n");
251
487
  }
@@ -255,121 +491,76 @@ export function resolveLocalConceptQuestion(text = "", manifest = {}) {
255
491
  const lower = input.toLowerCase();
256
492
  if (!lower) return null;
257
493
 
258
- const mentionsRoundtable = /\bround[\s-]?table\b/.test(lower);
259
494
  const asksWhatIs = /^(what('| i)?s|what is|tell me about|explain)\b/.test(lower) || /\bwhat is\b/.test(lower);
260
- const asksHowToUseRoundtable = mentionsRoundtable && /\b(how do i|how to|add to|join|use|set up|enable|create)\b/.test(lower);
261
- const asksHowToInvitePartner = /\b(invite|add)\b/.test(lower) && /\b(partner|teammate|teammates|teammember|collaborator|member)\b/.test(lower);
262
- const asksHowToShareProject = /\b(how do i|how to|share)\b/.test(lower) && /\b(this project|project)\b/.test(lower);
263
- const asksHowToMakeProject = /\b(how do i|how to|make|create|start)\b/.test(lower) && /\b(new project|project)\b/.test(lower);
264
- const sourcesFor = (...keys) => keys.map((key) => manifest.sourceHints?.[key]).filter(Boolean);
265
- const withSources = (lines, sources = []) => [
266
- ...lines,
267
- ...(sources.length ? [`sources: ${sources.join(", ")}`] : [])
268
- ].join("\n");
269
- if (mentionsRoundtable && asksWhatIs) {
270
- const sources = sourcesFor("roundtableDocs", "sharedState", "roundtable");
271
- return withSources([
272
- "Roundtable is Waterbrother’s shared-project collaboration model.",
273
- "It ties one project to one shared room with explicit room modes, member roles, operator lock, tasks, invites, and shared runtime selection."
274
- ], sources);
275
- }
276
-
277
- if (asksHowToUseRoundtable) {
278
- const sources = sourcesFor("roundtableDocs", "sharedState", "roundtable");
279
- return withSources([
280
- "To add to Roundtable, first enable shared-project mode in the repo with `waterbrother project share`.",
281
- "Then use `waterbrother room invite <member-id> [owner|editor|observer]` to create member invites, `waterbrother room tasks` / `waterbrother room task add <text>` to work the shared backlog, and `waterbrother room mode <chat|plan|execute>` plus `waterbrother room claim` when you want one operator to execute.",
282
- "In Telegram, the equivalent shared-room commands are `/room`, `/invite`, `/tasks`, `/task add`, `/mode`, `/claim`, and `/accept-invite`."
283
- ], sources);
284
- }
285
-
286
- if (asksHowToInvitePartner) {
287
- const sources = sourcesFor("roundtableDocs", "telegramDocs", "sharedState");
288
- return withSources([
289
- "To collaborate with a Telegram partner, there are two separate steps.",
290
- "First, add the human to the Telegram group/chat itself using normal Telegram controls.",
291
- "Second, add them to the Waterbrother shared room with `waterbrother room invite <member-id> [owner|editor|observer]` or `/invite <user-id> [owner|editor|observer]`.",
292
- "Then the invited person accepts with `waterbrother room invite accept <invite-id>` or `/accept-invite <invite-id>`.",
293
- "If this repo is not shared yet, start with `waterbrother project share`."
294
- ], sources);
295
- }
296
-
297
- if (asksHowToShareProject) {
298
- const sources = sourcesFor("roundtableDocs", "sharedState");
299
- return withSources([
300
- "To share the current project, run `waterbrother project share` in the repo.",
301
- "In an interactive terminal, Waterbrother will guide room mode, room runtime profile, and the first invite.",
302
- "After that, use `waterbrother room status`, `waterbrother room invite <member-id> [owner|editor|observer]`, and `waterbrother room mode <chat|plan|execute>` to manage collaboration."
303
- ], sources);
304
- }
495
+ const asksHow = /\b(how do i|how to|use|set up|enable|create|share|join|invite|make|start|add)\b/.test(lower);
496
+ const asksCapabilities = /\bwhat can you do\b/.test(lower) || /\bcapabilities\b/.test(lower) || /\bwhat do you support\b/.test(lower);
497
+ const asksProjectMap = /\bwhat is this project\b/.test(lower) || /\bwhat repo is this\b/.test(lower) || /\bwhat codebase is this\b/.test(lower);
498
+ const asksIdentity = /\bwhat is waterbrother\b/.test(lower) || /\bwho are you\b/.test(lower) || /\bwhat are you\b/.test(lower);
499
+ const mentionsRoundtable = /\bround[\s-]?table\b/.test(lower);
500
+ const mentionsSharedRoom = /\bshared room\b/.test(lower) || /\bshared project\b/.test(lower);
305
501
 
306
- if (asksHowToMakeProject) {
307
- const sources = sourcesFor("roundtableDocs", "telegramDocs", "readme");
308
- return withSources([
309
- "To make a new project from the TUI, use `/new-project <name>`.",
310
- "That creates a folder on `~/Desktop/<name>`, switches the live session into it, and now asks whether the new project should be shared.",
311
- "From Telegram, use `/new-project <name>` to create the folder and switch the linked session there. Then use `waterbrother project share` or the TUI sharing prompt if you want collaboration enabled."
312
- ], sources);
313
- }
502
+ const commands = manifest.knowledge?.commands || buildCommandRegistry({ sourceHints: manifest.sourceHints || {}, sharedRoom: manifest.sharedRoom || {} });
503
+ const capabilities = manifest.knowledge?.capabilities || buildCapabilityRegistry({ sourceHints: manifest.sourceHints || {}, sharedRoom: manifest.sharedRoom || {} });
504
+ const workflows = manifest.knowledge?.workflows || buildWorkflowRegistry({ sourceHints: manifest.sourceHints || {} });
505
+ const roomState = manifest.knowledge?.harness?.sharedRoomSummary || summarizeRoomState(manifest.sharedRoom || {});
506
+ const matchingWorkflows = findBestEntries(input, workflows, asksHow ? 2 : 4);
507
+ const matchingCommands = findBestEntries(input, commands, 2);
508
+ const matchingCapabilities = findBestEntries(input, capabilities, 2);
314
509
 
315
- if ((/\bshared project\b/.test(lower) || /\bshared room\b/.test(lower) || /\broom mode\b/.test(lower)) && asksWhatIs) {
316
- const sources = sourcesFor("sharedState", "roundtableDocs", "roundtable");
317
- return withSources([
318
- "A shared project links the current repo to one collaboration room with explicit member roles, room mode, operator lock, tasks, invites, and shared runtime state.",
319
- `Current room mode: ${manifest.sharedRoom?.roomMode || "chat"}. Shared enabled: ${manifest.sharedRoom?.enabled ? "yes" : "no"}.`
320
- ], sources);
321
- }
322
-
323
- if ((/\bwhat can you do\b/.test(lower) || /\bcapabilities\b/.test(lower) || /\bwhat do you support\b/.test(lower))) {
510
+ if (asksCapabilities) {
324
511
  return formatCapabilitiesSummary(manifest);
325
512
  }
326
513
 
327
- if ((/\bwhat is this project\b/.test(lower) || /\bwhat repo is this\b/.test(lower) || /\bwhat codebase is this\b/.test(lower))) {
514
+ if (asksProjectMap) {
328
515
  return formatProjectMap(manifest);
329
516
  }
330
517
 
331
- if (/\bwhat is waterbrother\b/.test(lower) || /\bwho are you\b/.test(lower) || /\bwhat are you\b/.test(lower)) {
518
+ if (asksIdentity) {
332
519
  return formatAboutWaterbrother(manifest);
333
520
  }
334
521
 
335
- if (/\btelegram\b/.test(lower) && asksWhatIs) {
336
- const sources = sourcesFor("telegramDocs", "sharedState");
337
- return withSources([
338
- "Telegram is Waterbrother’s first live messaging adapter.",
339
- "It supports pairing, live TUI bridging, shared-room commands, task operations, and room execution gating."
340
- ], sources);
341
- }
342
-
343
- if ((/\bgateway\b/.test(lower) || /\bmessaging gateway\b/.test(lower)) && asksWhatIs) {
344
- const sources = sourcesFor("telegramDocs", "sharedState");
345
- return withSources([
346
- "The gateway is Waterbrother’s channel runtime for messaging surfaces like Telegram.",
347
- "It handles pairing, routing, live TUI bridging, remote sessions, shared-room commands, and delivery state."
348
- ], sources);
522
+ if (asksWhatIs && mentionsRoundtable) {
523
+ return renderConceptAnswer({
524
+ subject: "Roundtable is Waterbrother's shared-project collaboration model.",
525
+ capability: matchingCapabilities[0] || capabilities.find((entry) => entry.id === "shared-projects") || null,
526
+ commands: matchingCommands.length ? matchingCommands : commands.filter((entry) => ["room-status", "room-membership", "room-tasks"].includes(entry.id)),
527
+ roomState,
528
+ extra: ["It ties one project to one room with explicit roles, room modes, an operator lock, Roundtable tasks, and shared runtime state."]
529
+ });
349
530
  }
350
531
 
351
- if ((/\bruntime profile\b/.test(lower) || /\bruntime profiles\b/.test(lower)) && asksWhatIs) {
352
- const sources = sourcesFor("readme", "sharedState");
353
- return withSources([
354
- "Runtime profiles are saved Waterbrother provider/model/runtime configurations.",
355
- "A shared room can also pin a room-level runtime profile that Telegram execution inherits when present."
356
- ], sources);
532
+ if (asksWhatIs && mentionsSharedRoom) {
533
+ return renderConceptAnswer({
534
+ subject: "A shared project links the current repo to one collaboration room with explicit shared-room state.",
535
+ capability: matchingCapabilities[0] || capabilities.find((entry) => entry.id === "shared-projects") || null,
536
+ commands: matchingCommands.length ? matchingCommands : commands.filter((entry) => ["room-status", "room-mode", "room-operator"].includes(entry.id)),
537
+ roomState,
538
+ extra: ["That shared room carries member roles, room mode, operator lock, tasks, invites, and shared runtime state."]
539
+ });
357
540
  }
358
541
 
359
- if ((/\bapproval\b/.test(lower) || /\bapprovals\b/.test(lower)) && asksWhatIs) {
360
- const sources = sourcesFor("readme", "telegramDocs");
361
- return withSources([
362
- "Approvals are Waterbrother’s safety gate for risky actions and scope changes.",
363
- "Local TUI runs can use the normal approval flow, while Telegram fallback runs still use approval=never when no live TUI host is attached."
364
- ], sources);
542
+ if (asksHow && matchingWorkflows.length) {
543
+ return renderWorkflowAnswer(
544
+ matchingWorkflows[0],
545
+ roomState,
546
+ matchingCapabilities[0] || null,
547
+ matchingCommands
548
+ );
365
549
  }
366
550
 
367
- if ((/\bsession\b/.test(lower) || /\bsessions\b/.test(lower)) && asksWhatIs) {
368
- const sources = sourcesFor("readme", "sharedState");
369
- return withSources([
370
- "Sessions are Waterbrother’s persisted work threads.",
371
- "They store cwd, runtime state, messages, and can be resumed locally or through Telegram-linked remote control."
372
- ], sources);
551
+ if (asksWhatIs || matchingCommands.length || matchingCapabilities.length) {
552
+ const subject = asksWhatIs
553
+ ? `Local Waterbrother context for: ${input}`
554
+ : "Local Waterbrother context";
555
+ return renderConceptAnswer({
556
+ subject,
557
+ capability: matchingCapabilities[0] || null,
558
+ commands: matchingCommands,
559
+ roomState,
560
+ extra: manifest.runtime?.provider || manifest.runtime?.model
561
+ ? [`Active runtime: ${manifest.runtime.provider || "unknown"}/${manifest.runtime.model || "unknown"}.`]
562
+ : []
563
+ });
373
564
  }
374
565
 
375
566
  return null;
@@ -456,6 +456,17 @@ export async function listSharedInvites(cwd) {
456
456
  return project.pendingInvites || [];
457
457
  }
458
458
 
459
+ export async function listSharedEvents(cwd, limit = 12) {
460
+ const project = await loadSharedProject(cwd);
461
+ requireSharedProject(project);
462
+ const max = Number.isFinite(Number(limit)) && Number(limit) > 0 ? Math.max(1, Math.floor(Number(limit))) : 12;
463
+ return Array.isArray(project.recentEvents)
464
+ ? [...project.recentEvents]
465
+ .sort((a, b) => String(b.createdAt || "").localeCompare(String(a.createdAt || "")))
466
+ .slice(0, max)
467
+ : [];
468
+ }
469
+
459
470
  export async function upsertSharedMember(cwd, member = {}, options = {}) {
460
471
  const existing = await loadSharedProject(cwd);
461
472
  requireOwner(existing, options.actorId);